From 823ecac8c0dbbcdac5f8cc918086c5ae216c22a5 Mon Sep 17 00:00:00 2001 From: Tatiana Villa Ema Date: Mon, 27 Apr 2026 21:42:42 +0200 Subject: [PATCH] frontend u backend --- backend/.dockerignore | 7 + backend/.env.example | 19 + backend/.gitignore | 16 + backend/.mvn/jvm.config | 10 + backend/Dockerfile | 34 ++ backend/docker-compose.yml | 47 +++ backend/docker/mysql-init.sql | 27 ++ backend/pom.xml | 136 +++++++ .../es/recursoscatolicos/DataInitializer.java | 57 +++ .../RecursosCatolicosApplication.java | 12 + .../recursoscatolicos/auth/AuthResponse.java | 13 + .../recursoscatolicos/auth/LoginRequest.java | 16 + .../auth/RegisterRequest.java | 27 ++ .../controller/AuthController.java | 42 +++ .../controller/DiarioController.java | 69 ++++ .../controller/DifuntoPersonalController.java | 58 +++ .../controller/IntencionController.java | 90 +++++ .../controller/ParroquiaController.java | 31 ++ .../dto/DifuntoPersonalDTO.java | 24 ++ .../dto/DifuntoPersonalRequest.java | 15 + .../dto/EntradaDiarioDTO.java | 29 ++ .../dto/EntradaDiarioRequest.java | 21 ++ .../es/recursoscatolicos/dto/GrupoDTO.java | 19 + .../recursoscatolicos/dto/IntencionDTO.java | 30 ++ .../dto/IntencionRequest.java | 20 + .../recursoscatolicos/dto/ParroquiaDTO.java | 19 + .../es/recursoscatolicos/dto/UsuarioDTO.java | 27 ++ .../es/recursoscatolicos/model/Ambito.java | 7 + .../model/DifuntoPersonal.java | 34 ++ .../model/EntradaDiario.java | 47 +++ .../es/recursoscatolicos/model/Grupo.java | 27 ++ .../es/recursoscatolicos/model/Intencion.java | 45 +++ .../es/recursoscatolicos/model/Parroquia.java | 26 ++ .../recursoscatolicos/model/TipoUsuario.java | 7 + .../es/recursoscatolicos/model/Usuario.java | 71 ++++ .../repository/DifuntoPersonalRepository.java | 11 + .../repository/EntradaDiarioRepository.java | 15 + .../repository/GrupoRepository.java | 11 + .../repository/IntencionRepository.java | 16 + .../repository/ParroquiaRepository.java | 7 + .../repository/UsuarioRepository.java | 13 + .../security/JwtAuthFilter.java | 64 ++++ .../security/SecurityConfig.java | 84 +++++ .../security/UserDetailsServiceImpl.java | 21 ++ .../service/AuthService.java | 82 +++++ .../service/DiarioService.java | 70 ++++ .../service/DifuntoPersonalService.java | 53 +++ .../service/IntencionService.java | 90 +++++ .../recursoscatolicos/service/JwtService.java | 63 ++++ .../service/ParroquiaService.java | 31 ++ .../src/main/resources/application.properties | 59 +++ backend/src/main/resources/db/init.sql | 43 +++ biblioteca-cristiana20260120.html | 50 --- .../biblioteca-cristiana.html | 1 + {css => frontend/css}/biblioteca.css | 0 frontend/css/diario.css | 346 ++++++++++++++++++ {css => frontend/css}/estilos.css | 193 +++++++++- {css => frontend/css}/intenciones.css | 0 {css => frontend/css}/login.css | 0 {css => frontend/css}/oraciones-basicas.css | 0 {css => frontend/css}/register.css | 0 .../data}/SpanishRevisedRVR1960Bible.xml | 0 {data => frontend/data}/anno-liturgico.md | 0 {data => frontend/data}/biblia/libro1.xml | 0 .../data}/calendario-liturgico-25-26.pdf | Bin .../data}/calendario-liturgico.json | 0 .../data}/colores-liturgicos.json | 0 {data => frontend/data}/difuntos.json | 0 {data => frontend/data}/intenciones.json | 0 {data => frontend/data}/libros.json | 0 {data => frontend/data}/salmos.json | 0 {data => frontend/data}/santos.json | 0 frontend/diario-oracion.html | 86 +++++ header.html => frontend/header.html | 1 + {img => frontend/img}/biblia.jpg | Bin {img => frontend/img}/biblioteca.jpg | Bin {img => frontend/img}/dolorosos1.jpg | Bin {img => frontend/img}/dolorosos2.jpg | Bin {img => frontend/img}/dolorosos3.jpg | Bin {img => frontend/img}/dolorosos4.jpg | Bin {img => frontend/img}/dolorosos5.jpg | Bin {img => frontend/img}/favicon.png | Bin {img => frontend/img}/faviconClaro.png | Bin {img => frontend/img}/gloriosos1.jpg | Bin {img => frontend/img}/gloriosos2.jpg | Bin {img => frontend/img}/gloriosos3.jpg | Bin {img => frontend/img}/gloriosos4.jpg | Bin {img => frontend/img}/gloriosos5.jpg | Bin {img => frontend/img}/gozosos1.jpg | Bin {img => frontend/img}/gozosos2.jpg | Bin {img => frontend/img}/gozosos3.jpg | Bin {img => frontend/img}/gozosos4.jpg | Bin {img => frontend/img}/gozosos5.jpg | Bin {img => frontend/img}/iconos/cruz.png | Bin {img => frontend/img}/iconos/flor.png | Bin {img => frontend/img}/iconos/vela.png | Bin {img => frontend/img}/libros/biblia-cee.jpg | Bin {img => frontend/img}/libros/biblia-cee.png | Bin .../img}/libros/catecismo-astete.jpg | Bin .../img}/libros/catecismo-astete.png | Bin {img => frontend/img}/luminosos1.jpg | Bin {img => frontend/img}/luminosos2.jpg | Bin {img => frontend/img}/luminosos3.jpg | Bin {img => frontend/img}/luminosos4.jpg | Bin {img => frontend/img}/luminosos5.jpg | Bin {img => frontend/img}/oraciones.jpg | Bin {img => frontend/img}/peliculas.jpg | Bin {img => frontend/img}/rosario.jpg | Bin {img => frontend/img}/santos.jpg | Bin index.html => frontend/index.html | 12 +- intenciones.html => frontend/intenciones.html | 0 {js => frontend/js}/api-config.js | 2 +- frontend/js/auth.js | 297 +++++++++++++++ {js => frontend/js}/biblioteca.js | 0 {js => frontend/js}/codigo.js | 49 +++ frontend/js/diario.js | 310 ++++++++++++++++ {js => frontend/js}/header.js | 0 {js => frontend/js}/intenciones.js | 70 +++- {js => frontend/js}/login.js | 0 {js => frontend/js}/register.js | 0 {js => frontend/js}/rosario.js | 0 {js => frontend/js}/rosario20260118.js | 0 login.html => frontend/login.html | 0 frontend/nginx.conf | 21 ++ .../oraciones-basicas.html | 1 + register.html => frontend/register.html | 0 rosario.html => frontend/rosario.html | 1 + js/auth.js | 54 --- prueba.html | 14 - 129 files changed, 3277 insertions(+), 143 deletions(-) create mode 100644 backend/.dockerignore create mode 100644 backend/.env.example create mode 100644 backend/.gitignore create mode 100644 backend/.mvn/jvm.config create mode 100644 backend/Dockerfile create mode 100644 backend/docker-compose.yml create mode 100644 backend/docker/mysql-init.sql create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/es/recursoscatolicos/DataInitializer.java create mode 100644 backend/src/main/java/es/recursoscatolicos/RecursosCatolicosApplication.java create mode 100644 backend/src/main/java/es/recursoscatolicos/auth/AuthResponse.java create mode 100644 backend/src/main/java/es/recursoscatolicos/auth/LoginRequest.java create mode 100644 backend/src/main/java/es/recursoscatolicos/auth/RegisterRequest.java create mode 100644 backend/src/main/java/es/recursoscatolicos/controller/AuthController.java create mode 100644 backend/src/main/java/es/recursoscatolicos/controller/DiarioController.java create mode 100644 backend/src/main/java/es/recursoscatolicos/controller/DifuntoPersonalController.java create mode 100644 backend/src/main/java/es/recursoscatolicos/controller/IntencionController.java create mode 100644 backend/src/main/java/es/recursoscatolicos/controller/ParroquiaController.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/DifuntoPersonalDTO.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/DifuntoPersonalRequest.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/EntradaDiarioDTO.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/EntradaDiarioRequest.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/GrupoDTO.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/IntencionDTO.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/IntencionRequest.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/ParroquiaDTO.java create mode 100644 backend/src/main/java/es/recursoscatolicos/dto/UsuarioDTO.java create mode 100644 backend/src/main/java/es/recursoscatolicos/model/Ambito.java create mode 100644 backend/src/main/java/es/recursoscatolicos/model/DifuntoPersonal.java create mode 100644 backend/src/main/java/es/recursoscatolicos/model/EntradaDiario.java create mode 100644 backend/src/main/java/es/recursoscatolicos/model/Grupo.java create mode 100644 backend/src/main/java/es/recursoscatolicos/model/Intencion.java create mode 100644 backend/src/main/java/es/recursoscatolicos/model/Parroquia.java create mode 100644 backend/src/main/java/es/recursoscatolicos/model/TipoUsuario.java create mode 100644 backend/src/main/java/es/recursoscatolicos/model/Usuario.java create mode 100644 backend/src/main/java/es/recursoscatolicos/repository/DifuntoPersonalRepository.java create mode 100644 backend/src/main/java/es/recursoscatolicos/repository/EntradaDiarioRepository.java create mode 100644 backend/src/main/java/es/recursoscatolicos/repository/GrupoRepository.java create mode 100644 backend/src/main/java/es/recursoscatolicos/repository/IntencionRepository.java create mode 100644 backend/src/main/java/es/recursoscatolicos/repository/ParroquiaRepository.java create mode 100644 backend/src/main/java/es/recursoscatolicos/repository/UsuarioRepository.java create mode 100644 backend/src/main/java/es/recursoscatolicos/security/JwtAuthFilter.java create mode 100644 backend/src/main/java/es/recursoscatolicos/security/SecurityConfig.java create mode 100644 backend/src/main/java/es/recursoscatolicos/security/UserDetailsServiceImpl.java create mode 100644 backend/src/main/java/es/recursoscatolicos/service/AuthService.java create mode 100644 backend/src/main/java/es/recursoscatolicos/service/DiarioService.java create mode 100644 backend/src/main/java/es/recursoscatolicos/service/DifuntoPersonalService.java create mode 100644 backend/src/main/java/es/recursoscatolicos/service/IntencionService.java create mode 100644 backend/src/main/java/es/recursoscatolicos/service/JwtService.java create mode 100644 backend/src/main/java/es/recursoscatolicos/service/ParroquiaService.java create mode 100644 backend/src/main/resources/application.properties create mode 100644 backend/src/main/resources/db/init.sql delete mode 100755 biblioteca-cristiana20260120.html rename biblioteca-cristiana.html => frontend/biblioteca-cristiana.html (96%) mode change 100755 => 100644 rename {css => frontend/css}/biblioteca.css (100%) mode change 100755 => 100644 create mode 100644 frontend/css/diario.css rename {css => frontend/css}/estilos.css (74%) mode change 100755 => 100644 rename {css => frontend/css}/intenciones.css (100%) mode change 100755 => 100644 rename {css => frontend/css}/login.css (100%) mode change 100755 => 100644 rename {css => frontend/css}/oraciones-basicas.css (100%) mode change 100755 => 100644 rename {css => frontend/css}/register.css (100%) mode change 100755 => 100644 rename {data => frontend/data}/SpanishRevisedRVR1960Bible.xml (100%) rename {data => frontend/data}/anno-liturgico.md (100%) mode change 100755 => 100644 rename {data => frontend/data}/biblia/libro1.xml (100%) rename {data => frontend/data}/calendario-liturgico-25-26.pdf (100%) mode change 100755 => 100644 rename {data => frontend/data}/calendario-liturgico.json (100%) mode change 100755 => 100644 rename {data => frontend/data}/colores-liturgicos.json (100%) mode change 100755 => 100644 rename {data => frontend/data}/difuntos.json (100%) mode change 100755 => 100644 rename {data => frontend/data}/intenciones.json (100%) mode change 100755 => 100644 rename {data => frontend/data}/libros.json (100%) mode change 100755 => 100644 rename {data => frontend/data}/salmos.json (100%) mode change 100755 => 100644 rename {data => frontend/data}/santos.json (100%) mode change 100755 => 100644 create mode 100644 frontend/diario-oracion.html rename header.html => frontend/header.html (96%) mode change 100755 => 100644 rename {img => frontend/img}/biblia.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/biblioteca.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/dolorosos1.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/dolorosos2.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/dolorosos3.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/dolorosos4.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/dolorosos5.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/favicon.png (100%) mode change 100755 => 100644 rename {img => frontend/img}/faviconClaro.png (100%) mode change 100755 => 100644 rename {img => frontend/img}/gloriosos1.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gloriosos2.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gloriosos3.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gloriosos4.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gloriosos5.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gozosos1.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gozosos2.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gozosos3.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gozosos4.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/gozosos5.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/iconos/cruz.png (100%) mode change 100755 => 100644 rename {img => frontend/img}/iconos/flor.png (100%) mode change 100755 => 100644 rename {img => frontend/img}/iconos/vela.png (100%) mode change 100755 => 100644 rename {img => frontend/img}/libros/biblia-cee.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/libros/biblia-cee.png (100%) mode change 100755 => 100644 rename {img => frontend/img}/libros/catecismo-astete.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/libros/catecismo-astete.png (100%) mode change 100755 => 100644 rename {img => frontend/img}/luminosos1.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/luminosos2.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/luminosos3.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/luminosos4.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/luminosos5.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/oraciones.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/peliculas.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/rosario.jpg (100%) mode change 100755 => 100644 rename {img => frontend/img}/santos.jpg (100%) mode change 100755 => 100644 rename index.html => frontend/index.html (88%) mode change 100755 => 100644 rename intenciones.html => frontend/intenciones.html (100%) mode change 100755 => 100644 rename {js => frontend/js}/api-config.js (95%) create mode 100644 frontend/js/auth.js rename {js => frontend/js}/biblioteca.js (100%) mode change 100755 => 100644 rename {js => frontend/js}/codigo.js (85%) mode change 100755 => 100644 create mode 100644 frontend/js/diario.js rename {js => frontend/js}/header.js (100%) mode change 100755 => 100644 rename {js => frontend/js}/intenciones.js (83%) mode change 100755 => 100644 rename {js => frontend/js}/login.js (100%) mode change 100755 => 100644 rename {js => frontend/js}/register.js (100%) mode change 100755 => 100644 rename {js => frontend/js}/rosario.js (100%) mode change 100755 => 100644 rename {js => frontend/js}/rosario20260118.js (100%) mode change 100755 => 100644 rename login.html => frontend/login.html (100%) mode change 100755 => 100644 create mode 100644 frontend/nginx.conf rename oraciones-basicas.html => frontend/oraciones-basicas.html (99%) mode change 100755 => 100644 rename register.html => frontend/register.html (100%) mode change 100755 => 100644 rename rosario.html => frontend/rosario.html (95%) mode change 100755 => 100644 delete mode 100644 js/auth.js delete mode 100644 prueba.html diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..3c7f8c8 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +target/ +.git/ +.gitignore +.env +*.md +src/test/ +docker/ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..30712c5 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,19 @@ +# ============================================================ +# Copia este archivo como .env y rellena los valores reales. +# NUNCA subas .env a git (está en .gitignore). +# ============================================================ + +# Contraseña de root de MySQL (solo al crear el contenedor) +MYSQL_ROOT_PASSWORD=cambia_esto + +# Contraseña del usuario rc_user +SPRING_DATASOURCE_PASSWORD=cambia_esto + +# Secreto JWT — mínimo 32 caracteres, aleatorio +JWT_SECRET=cambia_por_un_valor_largo_y_aleatorio_aqui_2026 + +# Expiración del token en ms (86400000 = 24h) +JWT_EXPIRATION=86400000 + +# Orígenes CORS permitidos (separados por coma, sin espacios) +CORS_ALLOWED_ORIGINS=https://recursos-catolicos.es,http://localhost:5500,http://127.0.0.1:5500 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..80eba61 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,16 @@ +target/ +*.class +*.jar +*.war + +# Variables de entorno con credenciales — NUNCA subir a git +.env + +# Logs +*.log +logs/ + +# IDE +.idea/ +*.iml +.vscode/ diff --git a/backend/.mvn/jvm.config b/backend/.mvn/jvm.config new file mode 100644 index 0000000..638a8a1 --- /dev/null +++ b/backend/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens java.base/java.lang=ALL-UNNAMED diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..eb49b53 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,34 @@ +# ============================================================ +# Stage 1: Build con Maven +# ============================================================ +FROM maven:3.9-eclipse-temurin-25 AS builder + +WORKDIR /app + +# Copiar solo el pom primero para aprovechar la caché de capas +COPY pom.xml . +RUN mvn dependency:go-offline -q + +# Copiar el código fuente y compilar +COPY src ./src +RUN mvn package -DskipTests -q + +# ============================================================ +# Stage 2: Runtime mínimo (solo JRE) +# ============================================================ +FROM eclipse-temurin:25-jre-noble + +WORKDIR /app + +# Usuario no-root por seguridad +RUN groupadd --system rcapp && useradd --system --gid rcapp rcapp + +COPY --from=builder /app/target/*.jar app.jar + +RUN chown rcapp:rcapp app.jar + +USER rcapp + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..de8dc35 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,47 @@ +services: + + # ────────────────────────────────────────── + # Base de datos MySQL 8 + # ────────────────────────────────────────── + db: + image: mysql:8.0 + container_name: rc_mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: recursoscatolicos + MYSQL_USER: rc_user + MYSQL_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} + volumes: + - mysql_data:/var/lib/mysql + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + + # ────────────────────────────────────────── + # Aplicación Spring Boot + # ────────────────────────────────────────── + app: + build: . + container_name: rc_app + restart: unless-stopped + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: "jdbc:mysql://db:3306/recursoscatolicos?useSSL=false&serverTimezone=Europe/Madrid&allowPublicKeyRetrieval=true&characterEncoding=UTF-8" + SPRING_DATASOURCE_USERNAME: rc_user + SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + JWT_EXPIRATION: ${JWT_EXPIRATION:-86400000} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS} + depends_on: + db: + condition: service_healthy + +volumes: + mysql_data: diff --git a/backend/docker/mysql-init.sql b/backend/docker/mysql-init.sql new file mode 100644 index 0000000..cc7324d --- /dev/null +++ b/backend/docker/mysql-init.sql @@ -0,0 +1,27 @@ +-- ============================================================ +-- Datos de ejemplo para el entorno Docker. +-- Este script se ejecuta automáticamente al crear el +-- contenedor MySQL por primera vez (docker-entrypoint-initdb.d). +-- La tabla la crea Hibernate al arrancar la app. +-- ============================================================ + +-- Esperamos a que Hibernate haya creado las tablas. +-- MySQL ejecuta este script antes de que la app arranque, +-- así que usamos INSERT IGNORE para que no falle si ya existen. + +-- Parroquias de ejemplo +INSERT IGNORE INTO parroquias (id, nombre, direccion) VALUES + (1, 'San José Obrero', 'Calle Mayor, 12, Madrid'), + (2, 'Nuestra Señora del Pilar', 'Avda. de la Paz, 5, Zaragoza'), + (3, 'Santa María de la Asunción', 'Plaza de la Iglesia, 1, Sevilla'); + +-- Grupos de ejemplo +INSERT IGNORE INTO grupos (id, nombre, parroquia_id) VALUES + (1, 'Catequesis infantil', 1), + (2, 'Catequesis de adultos', 1), + (3, 'Hogar de Misiones', 1), + (4, 'Cáritas parroquial', 1), + (5, 'Grupo de jóvenes', 1), + (6, 'Catequesis infantil', 2), + (7, 'Grupo de jóvenes', 2), + (8, 'Cáritas parroquial', 3); diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..9334050 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,136 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.14 + + + + es.recursoscatolicos + recursos-catolicos-api + 1.0.0 + jar + Recursos Católicos API + Backend para la web de recursos católicos + + + 25 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.mysql + mysql-connector-j + runtime + + + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + -J--add-opens=java.base/java.lang=ALL-UNNAMED + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/backend/src/main/java/es/recursoscatolicos/DataInitializer.java b/backend/src/main/java/es/recursoscatolicos/DataInitializer.java new file mode 100644 index 0000000..8adaf27 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/DataInitializer.java @@ -0,0 +1,57 @@ +package es.recursoscatolicos; + +import es.recursoscatolicos.model.Grupo; +import es.recursoscatolicos.model.Parroquia; +import es.recursoscatolicos.repository.GrupoRepository; +import es.recursoscatolicos.repository.ParroquiaRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class DataInitializer implements CommandLineRunner { + + private final ParroquiaRepository parroquiaRepository; + private final GrupoRepository grupoRepository; + + @Override + public void run(String... args) { + if (parroquiaRepository.count() > 0) { + return; // Datos ya cargados + } + + log.info("Cargando datos de ejemplo..."); + + Parroquia p1 = saveParroquia("San José Obrero", "Calle Mayor, 12, Madrid"); + Parroquia p2 = saveParroquia("Nuestra Señora del Pilar", "Avda. de la Paz, 5, Zaragoza"); + Parroquia p3 = saveParroquia("Santa María de la Asunción", "Plaza de la Iglesia, 1, Sevilla"); + + saveGrupo("Catequesis infantil", p1); + saveGrupo("Catequesis de adultos", p1); + saveGrupo("Hogar de Misiones", p1); + saveGrupo("Cáritas parroquial", p1); + saveGrupo("Grupo de jóvenes", p1); + saveGrupo("Catequesis infantil", p2); + saveGrupo("Grupo de jóvenes", p2); + saveGrupo("Cáritas parroquial", p3); + + log.info("Datos de ejemplo cargados correctamente."); + } + + private Parroquia saveParroquia(String nombre, String direccion) { + Parroquia p = new Parroquia(); + p.setNombre(nombre); + p.setDireccion(direccion); + return parroquiaRepository.save(p); + } + + private void saveGrupo(String nombre, Parroquia parroquia) { + Grupo g = new Grupo(); + g.setNombre(nombre); + g.setParroquia(parroquia); + grupoRepository.save(g); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/RecursosCatolicosApplication.java b/backend/src/main/java/es/recursoscatolicos/RecursosCatolicosApplication.java new file mode 100644 index 0000000..fcc2183 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/RecursosCatolicosApplication.java @@ -0,0 +1,12 @@ +package es.recursoscatolicos; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class RecursosCatolicosApplication { + + public static void main(String[] args) { + SpringApplication.run(RecursosCatolicosApplication.class, args); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/auth/AuthResponse.java b/backend/src/main/java/es/recursoscatolicos/auth/AuthResponse.java new file mode 100644 index 0000000..e0b5aac --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/auth/AuthResponse.java @@ -0,0 +1,13 @@ +package es.recursoscatolicos.auth; + +import es.recursoscatolicos.dto.UsuarioDTO; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AuthResponse { + + private String token; + private UsuarioDTO usuario; +} diff --git a/backend/src/main/java/es/recursoscatolicos/auth/LoginRequest.java b/backend/src/main/java/es/recursoscatolicos/auth/LoginRequest.java new file mode 100644 index 0000000..b09ed55 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/auth/LoginRequest.java @@ -0,0 +1,16 @@ +package es.recursoscatolicos.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class LoginRequest { + + @Email(message = "Email no válido") + @NotBlank(message = "El email es obligatorio") + private String email; + + @NotBlank(message = "La contraseña es obligatoria") + private String password; +} diff --git a/backend/src/main/java/es/recursoscatolicos/auth/RegisterRequest.java b/backend/src/main/java/es/recursoscatolicos/auth/RegisterRequest.java new file mode 100644 index 0000000..1abb4b7 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/auth/RegisterRequest.java @@ -0,0 +1,27 @@ +package es.recursoscatolicos.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class RegisterRequest { + + @NotBlank(message = "El nombre es obligatorio") + private String nombre; + + @Email(message = "Email no válido") + @NotBlank(message = "El email es obligatorio") + private String email; + + @NotBlank(message = "La contraseña es obligatoria") + @Size(min = 8, message = "La contraseña debe tener al menos 8 caracteres") + private String password; + + @NotBlank(message = "El tipo de usuario es obligatorio") + private String tipoUsuario; // "individual", "parroquia" o "grupo" + + private Long parroquiaId; // obligatorio si tipoUsuario = parroquia o grupo + private Long grupoId; // obligatorio si tipoUsuario = grupo +} diff --git a/backend/src/main/java/es/recursoscatolicos/controller/AuthController.java b/backend/src/main/java/es/recursoscatolicos/controller/AuthController.java new file mode 100644 index 0000000..e17d47c --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/controller/AuthController.java @@ -0,0 +1,42 @@ +package es.recursoscatolicos.controller; + +import es.recursoscatolicos.auth.AuthResponse; +import es.recursoscatolicos.auth.LoginRequest; +import es.recursoscatolicos.auth.RegisterRequest; +import es.recursoscatolicos.service.AuthService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + try { + AuthResponse response = authService.register(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + try { + AuthResponse response = authService.login(request); + return ResponseEntity.ok(response); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Credenciales incorrectas"); + } + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/controller/DiarioController.java b/backend/src/main/java/es/recursoscatolicos/controller/DiarioController.java new file mode 100644 index 0000000..ce13683 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/controller/DiarioController.java @@ -0,0 +1,69 @@ +package es.recursoscatolicos.controller; + +import es.recursoscatolicos.dto.EntradaDiarioDTO; +import es.recursoscatolicos.dto.EntradaDiarioRequest; +import es.recursoscatolicos.model.Usuario; +import es.recursoscatolicos.service.DiarioService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/diario") +@RequiredArgsConstructor +public class DiarioController { + + private final DiarioService diarioService; + + /** GET /diario — todas las entradas del usuario autenticado */ + @GetMapping + public ResponseEntity> getEntradas( + @AuthenticationPrincipal Usuario usuario) { + return ResponseEntity.ok(diarioService.getEntradas(usuario.getId())); + } + + /** GET /diario/{fecha} — entrada de un día concreto (yyyy-MM-dd) */ + @GetMapping("/{fecha}") + public ResponseEntity getEntradaPorFecha( + @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fecha, + @AuthenticationPrincipal Usuario usuario) { + EntradaDiarioDTO dto = diarioService.getEntradaPorFecha(usuario.getId(), fecha); + return dto != null ? ResponseEntity.ok(dto) : ResponseEntity.notFound().build(); + } + + /** POST /diario — crea o actualiza la entrada del día indicado (upsert) */ + @PostMapping + public ResponseEntity guardar( + @Valid @RequestBody EntradaDiarioRequest request, + @AuthenticationPrincipal Usuario usuario) { + try { + EntradaDiarioDTO dto = diarioService.guardar(request, usuario.getId()); + return ResponseEntity.status(HttpStatus.CREATED).body(dto); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + /** DELETE /diario/{id} — elimina una entrada por su id */ + @DeleteMapping("/{id}") + public ResponseEntity eliminar( + @PathVariable Long id, + @AuthenticationPrincipal Usuario usuario) { + try { + diarioService.eliminar(id, usuario.getId()); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } catch (AccessDeniedException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/controller/DifuntoPersonalController.java b/backend/src/main/java/es/recursoscatolicos/controller/DifuntoPersonalController.java new file mode 100644 index 0000000..4a0c995 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/controller/DifuntoPersonalController.java @@ -0,0 +1,58 @@ +package es.recursoscatolicos.controller; + +import es.recursoscatolicos.dto.DifuntoPersonalDTO; +import es.recursoscatolicos.dto.DifuntoPersonalRequest; +import es.recursoscatolicos.model.Usuario; +import es.recursoscatolicos.service.DifuntoPersonalService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/difuntos/personales") +@RequiredArgsConstructor +public class DifuntoPersonalController { + + private final DifuntoPersonalService difuntoService; + + /** GET /difuntos/personales */ + @GetMapping + public ResponseEntity> getDifuntos( + @AuthenticationPrincipal Usuario usuario) { + return ResponseEntity.ok(difuntoService.getDifuntos(usuario.getId())); + } + + /** POST /difuntos/personales */ + @PostMapping + public ResponseEntity crear( + @Valid @RequestBody DifuntoPersonalRequest request, + @AuthenticationPrincipal Usuario usuario) { + try { + DifuntoPersonalDTO dto = difuntoService.crear(request, usuario.getId()); + return ResponseEntity.status(HttpStatus.CREATED).body(dto); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + /** DELETE /difuntos/personales/{id} */ + @DeleteMapping("/{id}") + public ResponseEntity eliminar( + @PathVariable Long id, + @AuthenticationPrincipal Usuario usuario) { + try { + difuntoService.eliminar(id, usuario.getId()); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } catch (AccessDeniedException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/controller/IntencionController.java b/backend/src/main/java/es/recursoscatolicos/controller/IntencionController.java new file mode 100644 index 0000000..1e7400f --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/controller/IntencionController.java @@ -0,0 +1,90 @@ +package es.recursoscatolicos.controller; + +import es.recursoscatolicos.dto.IntencionDTO; +import es.recursoscatolicos.dto.IntencionRequest; +import es.recursoscatolicos.model.Usuario; +import es.recursoscatolicos.service.IntencionService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/intenciones") +@RequiredArgsConstructor +public class IntencionController { + + private final IntencionService intencionService; + + /** Devuelve las intenciones personales del usuario autenticado. */ + @GetMapping("/personales") + public ResponseEntity> getPersonales(@AuthenticationPrincipal Usuario usuario) { + return ResponseEntity.ok(intencionService.getPersonales(usuario.getId())); + } + + /** Devuelve las intenciones de una parroquia. Solo miembros de esa parroquia. */ + @GetMapping("/parroquia/{parroquiaId}") + public ResponseEntity> getDeParroquia( + @PathVariable Long parroquiaId, + @AuthenticationPrincipal Usuario usuario) { + + if (usuario.getParroquia() == null || !usuario.getParroquia().getId().equals(parroquiaId)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + return ResponseEntity.ok(intencionService.getDeParroquia(parroquiaId)); + } + + /** Devuelve las intenciones de un grupo. Solo miembros de ese grupo. */ + @GetMapping("/grupo/{grupoId}") + public ResponseEntity> getDeGrupo( + @PathVariable Long grupoId, + @AuthenticationPrincipal Usuario usuario) { + + boolean perteneceAlGrupo = usuario.getGrupos().stream() + .anyMatch(g -> g.getId().equals(grupoId)); + + if (!perteneceAlGrupo) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + return ResponseEntity.ok(intencionService.getDeGrupo(grupoId)); + } + + /** Crea una nueva intención. */ + @PostMapping + public ResponseEntity crear( + @Valid @RequestBody IntencionRequest request, + @AuthenticationPrincipal Usuario usuario) { + try { + IntencionDTO dto = intencionService.crear(request, usuario.getId()); + return ResponseEntity.status(HttpStatus.CREATED).body(dto); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + /** Elimina una intención. Solo el autor puede borrarla. */ + @DeleteMapping("/{id}") + public ResponseEntity eliminar( + @PathVariable Long id, + @AuthenticationPrincipal Usuario usuario) { + try { + intencionService.eliminar(id, usuario.getId()); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } catch (AccessDeniedException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/controller/ParroquiaController.java b/backend/src/main/java/es/recursoscatolicos/controller/ParroquiaController.java new file mode 100644 index 0000000..0d5a500 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/controller/ParroquiaController.java @@ -0,0 +1,31 @@ +package es.recursoscatolicos.controller; + +import es.recursoscatolicos.dto.GrupoDTO; +import es.recursoscatolicos.dto.ParroquiaDTO; +import es.recursoscatolicos.service.ParroquiaService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/parroquias") +@RequiredArgsConstructor +public class ParroquiaController { + + private final ParroquiaService parroquiaService; + + @GetMapping + public ResponseEntity> listarTodas() { + return ResponseEntity.ok(parroquiaService.listarTodas()); + } + + @GetMapping("/{id}/grupos") + public ResponseEntity> listarGrupos(@PathVariable Long id) { + return ResponseEntity.ok(parroquiaService.listarGruposPorParroquia(id)); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/DifuntoPersonalDTO.java b/backend/src/main/java/es/recursoscatolicos/dto/DifuntoPersonalDTO.java new file mode 100644 index 0000000..1924ab5 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/DifuntoPersonalDTO.java @@ -0,0 +1,24 @@ +package es.recursoscatolicos.dto; + +import es.recursoscatolicos.model.DifuntoPersonal; +import lombok.Data; + +@Data +public class DifuntoPersonalDTO { + + private Long id; + private String nombre; + private String nacimiento; + private String defuncion; + private String nota; + + public static DifuntoPersonalDTO from(DifuntoPersonal d) { + DifuntoPersonalDTO dto = new DifuntoPersonalDTO(); + dto.setId(d.getId()); + dto.setNombre(d.getNombre()); + dto.setNacimiento(d.getNacimiento()); + dto.setDefuncion(d.getDefuncion()); + dto.setNota(d.getNota()); + return dto; + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/DifuntoPersonalRequest.java b/backend/src/main/java/es/recursoscatolicos/dto/DifuntoPersonalRequest.java new file mode 100644 index 0000000..977ffe9 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/DifuntoPersonalRequest.java @@ -0,0 +1,15 @@ +package es.recursoscatolicos.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class DifuntoPersonalRequest { + + @NotBlank(message = "El nombre es obligatorio") + private String nombre; + + private String nacimiento; + private String defuncion; + private String nota; +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/EntradaDiarioDTO.java b/backend/src/main/java/es/recursoscatolicos/dto/EntradaDiarioDTO.java new file mode 100644 index 0000000..9e50c67 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/EntradaDiarioDTO.java @@ -0,0 +1,29 @@ +package es.recursoscatolicos.dto; + +import es.recursoscatolicos.model.EntradaDiario; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +public class EntradaDiarioDTO { + + private Long id; + private LocalDate fecha; + private String titulo; + private String texto; + private String estado; + private LocalDateTime actualizadoEn; + + public static EntradaDiarioDTO from(EntradaDiario e) { + EntradaDiarioDTO dto = new EntradaDiarioDTO(); + dto.setId(e.getId()); + dto.setFecha(e.getFecha()); + dto.setTitulo(e.getTitulo()); + dto.setTexto(e.getTexto()); + dto.setEstado(e.getEstado()); + dto.setActualizadoEn(e.getActualizadoEn()); + return dto; + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/EntradaDiarioRequest.java b/backend/src/main/java/es/recursoscatolicos/dto/EntradaDiarioRequest.java new file mode 100644 index 0000000..d470291 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/EntradaDiarioRequest.java @@ -0,0 +1,21 @@ +package es.recursoscatolicos.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class EntradaDiarioRequest { + + @NotNull(message = "La fecha es obligatoria") + private LocalDate fecha; + + private String titulo; + + @NotBlank(message = "El texto no puede estar vacío") + private String texto; + + private String estado; +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/GrupoDTO.java b/backend/src/main/java/es/recursoscatolicos/dto/GrupoDTO.java new file mode 100644 index 0000000..92f2491 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/GrupoDTO.java @@ -0,0 +1,19 @@ +package es.recursoscatolicos.dto; + +import es.recursoscatolicos.model.Grupo; +import lombok.Data; + +@Data +public class GrupoDTO { + + private Long id; + private String nombre; + + public static GrupoDTO from(Grupo g) { + if (g == null) return null; + GrupoDTO dto = new GrupoDTO(); + dto.setId(g.getId()); + dto.setNombre(g.getNombre()); + return dto; + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/IntencionDTO.java b/backend/src/main/java/es/recursoscatolicos/dto/IntencionDTO.java new file mode 100644 index 0000000..599784c --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/IntencionDTO.java @@ -0,0 +1,30 @@ +package es.recursoscatolicos.dto; + +import es.recursoscatolicos.model.Intencion; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class IntencionDTO { + + private Long id; + private String texto; + private String icono; + private String ambito; + private String autorNombre; + private Long usuarioId; + private LocalDateTime fechaCreacion; + + public static IntencionDTO from(Intencion i) { + IntencionDTO dto = new IntencionDTO(); + dto.setId(i.getId()); + dto.setTexto(i.getTexto()); + dto.setIcono(i.getIcono()); + dto.setAmbito(i.getAmbito().name().toLowerCase()); + dto.setAutorNombre(i.getUsuario().getNombre()); + dto.setUsuarioId(i.getUsuario().getId()); + dto.setFechaCreacion(i.getFechaCreacion()); + return dto; + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/IntencionRequest.java b/backend/src/main/java/es/recursoscatolicos/dto/IntencionRequest.java new file mode 100644 index 0000000..3df696c --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/IntencionRequest.java @@ -0,0 +1,20 @@ +package es.recursoscatolicos.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class IntencionRequest { + + @NotBlank(message = "El texto no puede estar vacío") + private String texto; + + private String icono; + + @NotNull(message = "El ámbito es obligatorio") + private String ambito; // "personal", "parroquia" o "grupo" + + private Long parroquiaId; + private Long grupoId; +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/ParroquiaDTO.java b/backend/src/main/java/es/recursoscatolicos/dto/ParroquiaDTO.java new file mode 100644 index 0000000..84c3d43 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/ParroquiaDTO.java @@ -0,0 +1,19 @@ +package es.recursoscatolicos.dto; + +import es.recursoscatolicos.model.Parroquia; +import lombok.Data; + +@Data +public class ParroquiaDTO { + + private Long id; + private String nombre; + + public static ParroquiaDTO from(Parroquia p) { + if (p == null) return null; + ParroquiaDTO dto = new ParroquiaDTO(); + dto.setId(p.getId()); + dto.setNombre(p.getNombre()); + return dto; + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/dto/UsuarioDTO.java b/backend/src/main/java/es/recursoscatolicos/dto/UsuarioDTO.java new file mode 100644 index 0000000..0f30b1e --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/dto/UsuarioDTO.java @@ -0,0 +1,27 @@ +package es.recursoscatolicos.dto; + +import es.recursoscatolicos.model.Usuario; +import lombok.Data; + +import java.util.List; +import java.util.stream.Collectors; + +@Data +public class UsuarioDTO { + + private Long id; + private String nombre; + private ParroquiaDTO parroquia; + private List grupos; + + public static UsuarioDTO from(Usuario u) { + UsuarioDTO dto = new UsuarioDTO(); + dto.setId(u.getId()); + dto.setNombre(u.getNombre()); + dto.setParroquia(ParroquiaDTO.from(u.getParroquia())); + dto.setGrupos(u.getGrupos().stream() + .map(GrupoDTO::from) + .collect(Collectors.toList())); + return dto; + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/model/Ambito.java b/backend/src/main/java/es/recursoscatolicos/model/Ambito.java new file mode 100644 index 0000000..7b7ead2 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/model/Ambito.java @@ -0,0 +1,7 @@ +package es.recursoscatolicos.model; + +public enum Ambito { + PERSONAL, + PARROQUIA, + GRUPO +} diff --git a/backend/src/main/java/es/recursoscatolicos/model/DifuntoPersonal.java b/backend/src/main/java/es/recursoscatolicos/model/DifuntoPersonal.java new file mode 100644 index 0000000..3f33789 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/model/DifuntoPersonal.java @@ -0,0 +1,34 @@ +package es.recursoscatolicos.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "difuntos_personales") +@Data +@NoArgsConstructor +public class DifuntoPersonal { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "usuario_id", nullable = false) + private Usuario usuario; + + @Column(nullable = false, length = 150) + private String nombre; + + /** Formato: YYYY-MM-DD. Puede contener XXXX en el año si se desconoce. */ + @Column(length = 10) + private String nacimiento; + + /** Formato: YYYY-MM-DD. Puede contener XXXX en el año si se desconoce. */ + @Column(length = 10) + private String defuncion; + + @Column(length = 300) + private String nota; +} diff --git a/backend/src/main/java/es/recursoscatolicos/model/EntradaDiario.java b/backend/src/main/java/es/recursoscatolicos/model/EntradaDiario.java new file mode 100644 index 0000000..f7ad1d1 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/model/EntradaDiario.java @@ -0,0 +1,47 @@ +package es.recursoscatolicos.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Table(name = "entradas_diario", + uniqueConstraints = @UniqueConstraint(columnNames = {"usuario_id", "fecha"})) +@Data +@NoArgsConstructor +public class EntradaDiario { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "usuario_id", nullable = false) + private Usuario usuario; + + @Column(nullable = false) + private LocalDate fecha; + + @Column(length = 100) + private String titulo; + + @Column(nullable = false, columnDefinition = "TEXT") + private String texto; + + /** Estado espiritual: paz, gratitud, lucha, gozo, silencio */ + @Column(length = 30) + private String estado; + + @CreationTimestamp + @Column(name = "creado_en", updatable = false) + private LocalDateTime creadoEn; + + @UpdateTimestamp + @Column(name = "actualizado_en") + private LocalDateTime actualizadoEn; +} diff --git a/backend/src/main/java/es/recursoscatolicos/model/Grupo.java b/backend/src/main/java/es/recursoscatolicos/model/Grupo.java new file mode 100644 index 0000000..1632208 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/model/Grupo.java @@ -0,0 +1,27 @@ +package es.recursoscatolicos.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "grupos") +@Data +@NoArgsConstructor +public class Grupo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 150) + private String nombre; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parroquia_id", nullable = false) + private Parroquia parroquia; + + public Grupo(Long id) { + this.id = id; + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/model/Intencion.java b/backend/src/main/java/es/recursoscatolicos/model/Intencion.java new file mode 100644 index 0000000..042d465 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/model/Intencion.java @@ -0,0 +1,45 @@ +package es.recursoscatolicos.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "intenciones") +@Data +@NoArgsConstructor +public class Intencion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 500) + private String texto; + + @Column(length = 50) + private String icono; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Ambito ambito; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "usuario_id", nullable = false) + private Usuario usuario; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parroquia_id") + private Parroquia parroquia; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "grupo_id") + private Grupo grupo; + + @CreationTimestamp + @Column(name = "fecha_creacion", updatable = false) + private LocalDateTime fechaCreacion; +} diff --git a/backend/src/main/java/es/recursoscatolicos/model/Parroquia.java b/backend/src/main/java/es/recursoscatolicos/model/Parroquia.java new file mode 100644 index 0000000..2e3df47 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/model/Parroquia.java @@ -0,0 +1,26 @@ +package es.recursoscatolicos.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "parroquias") +@Data +@NoArgsConstructor +public class Parroquia { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 200) + private String nombre; + + @Column(length = 300) + private String direccion; + + public Parroquia(Long id) { + this.id = id; + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/model/TipoUsuario.java b/backend/src/main/java/es/recursoscatolicos/model/TipoUsuario.java new file mode 100644 index 0000000..8d3c664 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/model/TipoUsuario.java @@ -0,0 +1,7 @@ +package es.recursoscatolicos.model; + +public enum TipoUsuario { + INDIVIDUAL, + PARROQUIA, // pertenece a una parroquia (sin grupo específico) + GRUPO // pertenece a un grupo dentro de una parroquia +} diff --git a/backend/src/main/java/es/recursoscatolicos/model/Usuario.java b/backend/src/main/java/es/recursoscatolicos/model/Usuario.java new file mode 100644 index 0000000..4995e76 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/model/Usuario.java @@ -0,0 +1,71 @@ +package es.recursoscatolicos.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Entity +@Table(name = "usuarios") +@Data +@NoArgsConstructor +public class Usuario implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 150) + private String nombre; + + @Column(nullable = false, unique = true, length = 200) + private String email; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + @Column(name = "tipo_usuario", nullable = false) + private TipoUsuario tipoUsuario; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "parroquia_id") + private Parroquia parroquia; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "usuario_grupos", + joinColumns = @JoinColumn(name = "usuario_id"), + inverseJoinColumns = @JoinColumn(name = "grupo_id") + ) + private List grupos = new ArrayList<>(); + + // ── UserDetails ───────────────────────────────────────── + + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { return true; } + + @Override + public boolean isAccountNonLocked() { return true; } + + @Override + public boolean isCredentialsNonExpired() { return true; } + + @Override + public boolean isEnabled() { return true; } +} diff --git a/backend/src/main/java/es/recursoscatolicos/repository/DifuntoPersonalRepository.java b/backend/src/main/java/es/recursoscatolicos/repository/DifuntoPersonalRepository.java new file mode 100644 index 0000000..8c701ee --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/repository/DifuntoPersonalRepository.java @@ -0,0 +1,11 @@ +package es.recursoscatolicos.repository; + +import es.recursoscatolicos.model.DifuntoPersonal; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface DifuntoPersonalRepository extends JpaRepository { + + List findByUsuarioIdOrderByNombreAsc(Long usuarioId); +} diff --git a/backend/src/main/java/es/recursoscatolicos/repository/EntradaDiarioRepository.java b/backend/src/main/java/es/recursoscatolicos/repository/EntradaDiarioRepository.java new file mode 100644 index 0000000..d3058c7 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/repository/EntradaDiarioRepository.java @@ -0,0 +1,15 @@ +package es.recursoscatolicos.repository; + +import es.recursoscatolicos.model.EntradaDiario; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +public interface EntradaDiarioRepository extends JpaRepository { + + List findByUsuarioIdOrderByFechaDesc(Long usuarioId); + + Optional findByUsuarioIdAndFecha(Long usuarioId, LocalDate fecha); +} diff --git a/backend/src/main/java/es/recursoscatolicos/repository/GrupoRepository.java b/backend/src/main/java/es/recursoscatolicos/repository/GrupoRepository.java new file mode 100644 index 0000000..ce49a8a --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/repository/GrupoRepository.java @@ -0,0 +1,11 @@ +package es.recursoscatolicos.repository; + +import es.recursoscatolicos.model.Grupo; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface GrupoRepository extends JpaRepository { + + List findByParroquiaId(Long parroquiaId); +} diff --git a/backend/src/main/java/es/recursoscatolicos/repository/IntencionRepository.java b/backend/src/main/java/es/recursoscatolicos/repository/IntencionRepository.java new file mode 100644 index 0000000..71cd245 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/repository/IntencionRepository.java @@ -0,0 +1,16 @@ +package es.recursoscatolicos.repository; + +import es.recursoscatolicos.model.Ambito; +import es.recursoscatolicos.model.Intencion; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface IntencionRepository extends JpaRepository { + + List findByUsuarioIdAndAmbito(Long usuarioId, Ambito ambito); + + List findByParroquiaIdAndAmbito(Long parroquiaId, Ambito ambito); + + List findByGrupoIdAndAmbito(Long grupoId, Ambito ambito); +} diff --git a/backend/src/main/java/es/recursoscatolicos/repository/ParroquiaRepository.java b/backend/src/main/java/es/recursoscatolicos/repository/ParroquiaRepository.java new file mode 100644 index 0000000..59db79a --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/repository/ParroquiaRepository.java @@ -0,0 +1,7 @@ +package es.recursoscatolicos.repository; + +import es.recursoscatolicos.model.Parroquia; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ParroquiaRepository extends JpaRepository { +} diff --git a/backend/src/main/java/es/recursoscatolicos/repository/UsuarioRepository.java b/backend/src/main/java/es/recursoscatolicos/repository/UsuarioRepository.java new file mode 100644 index 0000000..1882c7c --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/repository/UsuarioRepository.java @@ -0,0 +1,13 @@ +package es.recursoscatolicos.repository; + +import es.recursoscatolicos.model.Usuario; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UsuarioRepository extends JpaRepository { + + Optional findByEmail(String email); + + boolean existsByEmail(String email); +} diff --git a/backend/src/main/java/es/recursoscatolicos/security/JwtAuthFilter.java b/backend/src/main/java/es/recursoscatolicos/security/JwtAuthFilter.java new file mode 100644 index 0000000..294affa --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/security/JwtAuthFilter.java @@ -0,0 +1,64 @@ +package es.recursoscatolicos.security; + +import es.recursoscatolicos.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + final String jwt = authHeader.substring(7); + String email; + + try { + email = jwtService.extractEmail(jwt); + } catch (Exception e) { + filterChain.doFilter(request, response); + return; + } + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/security/SecurityConfig.java b/backend/src/main/java/es/recursoscatolicos/security/SecurityConfig.java new file mode 100644 index 0000000..17a60ef --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/security/SecurityConfig.java @@ -0,0 +1,84 @@ +package es.recursoscatolicos.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthFilter jwtAuthFilter; + private final UserDetailsService userDetailsService; + + @Value("${cors.allowed-origins}") + private String allowedOrigins; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/**").permitAll() + .requestMatchers(HttpMethod.GET, "/parroquias/**").permitAll() + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(Arrays.asList(allowedOrigins.split(","))); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type")); + config.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + + private DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/security/UserDetailsServiceImpl.java b/backend/src/main/java/es/recursoscatolicos/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..d513969 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/security/UserDetailsServiceImpl.java @@ -0,0 +1,21 @@ +package es.recursoscatolicos.security; + +import es.recursoscatolicos.repository.UsuarioRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + + private final UsuarioRepository usuarioRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return usuarioRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado: " + email)); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/service/AuthService.java b/backend/src/main/java/es/recursoscatolicos/service/AuthService.java new file mode 100644 index 0000000..44db77b --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/service/AuthService.java @@ -0,0 +1,82 @@ +package es.recursoscatolicos.service; + +import es.recursoscatolicos.auth.AuthResponse; +import es.recursoscatolicos.auth.LoginRequest; +import es.recursoscatolicos.auth.RegisterRequest; +import es.recursoscatolicos.dto.UsuarioDTO; +import es.recursoscatolicos.model.Grupo; +import es.recursoscatolicos.model.Parroquia; +import es.recursoscatolicos.model.TipoUsuario; +import es.recursoscatolicos.model.Usuario; +import es.recursoscatolicos.repository.GrupoRepository; +import es.recursoscatolicos.repository.ParroquiaRepository; +import es.recursoscatolicos.repository.UsuarioRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final UsuarioRepository usuarioRepository; + private final ParroquiaRepository parroquiaRepository; + private final GrupoRepository grupoRepository; + private final JwtService jwtService; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + + @Transactional + public AuthResponse register(RegisterRequest request) { + if (usuarioRepository.existsByEmail(request.getEmail())) { + throw new IllegalArgumentException("El email ya está registrado"); + } + + TipoUsuario tipo; + try { + tipo = TipoUsuario.valueOf(request.getTipoUsuario().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Tipo de usuario no válido: " + request.getTipoUsuario()); + } + + Usuario usuario = new Usuario(); + usuario.setNombre(request.getNombre()); + usuario.setEmail(request.getEmail()); + usuario.setPassword(passwordEncoder.encode(request.getPassword())); + usuario.setTipoUsuario(tipo); + + if (request.getParroquiaId() != null) { + Parroquia parroquia = parroquiaRepository.findById(request.getParroquiaId()) + .orElseThrow(() -> new IllegalArgumentException("Parroquia no encontrada")); + usuario.setParroquia(parroquia); + } + + if (request.getGrupoId() != null) { + Grupo grupo = grupoRepository.findById(request.getGrupoId()) + .orElseThrow(() -> new IllegalArgumentException("Grupo no encontrado")); + List grupos = new ArrayList<>(); + grupos.add(grupo); + usuario.setGrupos(grupos); + } + + usuarioRepository.save(usuario); + String token = jwtService.generateToken(usuario); + return new AuthResponse(token, UsuarioDTO.from(usuario)); + } + + public AuthResponse login(LoginRequest request) { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) + ); + Usuario usuario = usuarioRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new IllegalArgumentException("Usuario no encontrado")); + String token = jwtService.generateToken(usuario); + return new AuthResponse(token, UsuarioDTO.from(usuario)); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/service/DiarioService.java b/backend/src/main/java/es/recursoscatolicos/service/DiarioService.java new file mode 100644 index 0000000..65a76d2 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/service/DiarioService.java @@ -0,0 +1,70 @@ +package es.recursoscatolicos.service; + +import es.recursoscatolicos.dto.EntradaDiarioDTO; +import es.recursoscatolicos.dto.EntradaDiarioRequest; +import es.recursoscatolicos.model.EntradaDiario; +import es.recursoscatolicos.model.Usuario; +import es.recursoscatolicos.repository.EntradaDiarioRepository; +import es.recursoscatolicos.repository.UsuarioRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class DiarioService { + + private final EntradaDiarioRepository diarioRepository; + private final UsuarioRepository usuarioRepository; + + /** Devuelve todas las entradas del usuario, ordenadas de más reciente a más antigua. */ + public List getEntradas(Long usuarioId) { + return diarioRepository.findByUsuarioIdOrderByFechaDesc(usuarioId) + .stream().map(EntradaDiarioDTO::from).collect(Collectors.toList()); + } + + /** Devuelve la entrada de una fecha concreta, o null si no existe. */ + public EntradaDiarioDTO getEntradaPorFecha(Long usuarioId, LocalDate fecha) { + return diarioRepository.findByUsuarioIdAndFecha(usuarioId, fecha) + .map(EntradaDiarioDTO::from) + .orElse(null); + } + + /** + * Crea o actualiza (upsert) la entrada del usuario para la fecha indicada. + * Si ya existe una entrada para ese día, la sobreescribe. + */ + @Transactional + public EntradaDiarioDTO guardar(EntradaDiarioRequest request, Long usuarioId) { + Usuario usuario = usuarioRepository.findById(usuarioId) + .orElseThrow(() -> new IllegalArgumentException("Usuario no encontrado")); + + EntradaDiario entrada = diarioRepository + .findByUsuarioIdAndFecha(usuarioId, request.getFecha()) + .orElseGet(EntradaDiario::new); + + entrada.setUsuario(usuario); + entrada.setFecha(request.getFecha()); + entrada.setTitulo(request.getTitulo()); + entrada.setTexto(request.getTexto()); + entrada.setEstado(request.getEstado()); + + return EntradaDiarioDTO.from(diarioRepository.save(entrada)); + } + + /** Elimina la entrada de una fecha concreta. Solo el propietario puede borrarla. */ + @Transactional + public void eliminar(Long entradaId, Long usuarioId) { + EntradaDiario entrada = diarioRepository.findById(entradaId) + .orElseThrow(() -> new IllegalArgumentException("Entrada no encontrada")); + if (!entrada.getUsuario().getId().equals(usuarioId)) { + throw new AccessDeniedException("No tienes permiso para eliminar esta entrada"); + } + diarioRepository.delete(entrada); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/service/DifuntoPersonalService.java b/backend/src/main/java/es/recursoscatolicos/service/DifuntoPersonalService.java new file mode 100644 index 0000000..a7b7559 --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/service/DifuntoPersonalService.java @@ -0,0 +1,53 @@ +package es.recursoscatolicos.service; + +import es.recursoscatolicos.dto.DifuntoPersonalDTO; +import es.recursoscatolicos.dto.DifuntoPersonalRequest; +import es.recursoscatolicos.model.DifuntoPersonal; +import es.recursoscatolicos.model.Usuario; +import es.recursoscatolicos.repository.DifuntoPersonalRepository; +import es.recursoscatolicos.repository.UsuarioRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class DifuntoPersonalService { + + private final DifuntoPersonalRepository difuntoRepository; + private final UsuarioRepository usuarioRepository; + + public List getDifuntos(Long usuarioId) { + return difuntoRepository.findByUsuarioIdOrderByNombreAsc(usuarioId) + .stream().map(DifuntoPersonalDTO::from).collect(Collectors.toList()); + } + + @Transactional + public DifuntoPersonalDTO crear(DifuntoPersonalRequest request, Long usuarioId) { + Usuario usuario = usuarioRepository.findById(usuarioId) + .orElseThrow(() -> new IllegalArgumentException("Usuario no encontrado")); + + DifuntoPersonal difunto = new DifuntoPersonal(); + difunto.setUsuario(usuario); + difunto.setNombre(request.getNombre()); + difunto.setNacimiento(request.getNacimiento()); + difunto.setDefuncion(request.getDefuncion()); + difunto.setNota(request.getNota()); + + return DifuntoPersonalDTO.from(difuntoRepository.save(difunto)); + } + + @Transactional + public void eliminar(Long difuntoId, Long usuarioId) { + DifuntoPersonal difunto = difuntoRepository.findById(difuntoId) + .orElseThrow(() -> new IllegalArgumentException("Difunto no encontrado")); + if (!difunto.getUsuario().getId().equals(usuarioId)) { + throw new AccessDeniedException("No tienes permiso para eliminar este difunto"); + } + difuntoRepository.delete(difunto); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/service/IntencionService.java b/backend/src/main/java/es/recursoscatolicos/service/IntencionService.java new file mode 100644 index 0000000..703fbea --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/service/IntencionService.java @@ -0,0 +1,90 @@ +package es.recursoscatolicos.service; + +import es.recursoscatolicos.dto.IntencionDTO; +import es.recursoscatolicos.dto.IntencionRequest; +import es.recursoscatolicos.model.Ambito; +import es.recursoscatolicos.model.Grupo; +import es.recursoscatolicos.model.Intencion; +import es.recursoscatolicos.model.Parroquia; +import es.recursoscatolicos.model.Usuario; +import es.recursoscatolicos.repository.GrupoRepository; +import es.recursoscatolicos.repository.IntencionRepository; +import es.recursoscatolicos.repository.ParroquiaRepository; +import es.recursoscatolicos.repository.UsuarioRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class IntencionService { + + private final IntencionRepository intencionRepository; + private final ParroquiaRepository parroquiaRepository; + private final GrupoRepository grupoRepository; + private final UsuarioRepository usuarioRepository; + + public List getPersonales(Long usuarioId) { + return intencionRepository.findByUsuarioIdAndAmbito(usuarioId, Ambito.PERSONAL) + .stream().map(IntencionDTO::from).collect(Collectors.toList()); + } + + public List getDeParroquia(Long parroquiaId) { + return intencionRepository.findByParroquiaIdAndAmbito(parroquiaId, Ambito.PARROQUIA) + .stream().map(IntencionDTO::from).collect(Collectors.toList()); + } + + public List getDeGrupo(Long grupoId) { + return intencionRepository.findByGrupoIdAndAmbito(grupoId, Ambito.GRUPO) + .stream().map(IntencionDTO::from).collect(Collectors.toList()); + } + + @Transactional + public IntencionDTO crear(IntencionRequest request, Long usuarioId) { + Usuario usuario = usuarioRepository.findById(usuarioId) + .orElseThrow(() -> new IllegalArgumentException("Usuario no encontrado")); + + Ambito ambito; + try { + ambito = Ambito.valueOf(request.getAmbito().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Ámbito no válido: " + request.getAmbito()); + } + + Intencion intencion = new Intencion(); + intencion.setTexto(request.getTexto()); + intencion.setIcono(request.getIcono() != null ? request.getIcono() : "cruz.png"); + intencion.setAmbito(ambito); + intencion.setUsuario(usuario); + + if (ambito == Ambito.PARROQUIA && request.getParroquiaId() != null) { + Parroquia parroquia = parroquiaRepository.findById(request.getParroquiaId()) + .orElseThrow(() -> new IllegalArgumentException("Parroquia no encontrada")); + intencion.setParroquia(parroquia); + } + + if (ambito == Ambito.GRUPO && request.getGrupoId() != null) { + Grupo grupo = grupoRepository.findById(request.getGrupoId()) + .orElseThrow(() -> new IllegalArgumentException("Grupo no encontrado")); + intencion.setGrupo(grupo); + } + + return IntencionDTO.from(intencionRepository.save(intencion)); + } + + @Transactional + public void eliminar(Long intencionId, Long usuarioId) { + Intencion intencion = intencionRepository.findById(intencionId) + .orElseThrow(() -> new IllegalArgumentException("Intención no encontrada")); + + if (!intencion.getUsuario().getId().equals(usuarioId)) { + throw new AccessDeniedException("No puedes eliminar una intención que no es tuya"); + } + + intencionRepository.delete(intencion); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/service/JwtService.java b/backend/src/main/java/es/recursoscatolicos/service/JwtService.java new file mode 100644 index 0000000..f506c2a --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/service/JwtService.java @@ -0,0 +1,63 @@ +package es.recursoscatolicos.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.function.Function; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + private SecretKey getSigningKey() { + byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String generateToken(UserDetails userDetails) { + return Jwts.builder() + .subject(userDetails.getUsername()) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + + public String extractEmail(String token) { + return extractClaim(token, Claims::getSubject); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String email = extractEmail(token); + return email.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private T extractClaim(String token, Function claimsResolver) { + final Claims claims = Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + return claimsResolver.apply(claims); + } +} diff --git a/backend/src/main/java/es/recursoscatolicos/service/ParroquiaService.java b/backend/src/main/java/es/recursoscatolicos/service/ParroquiaService.java new file mode 100644 index 0000000..84294aa --- /dev/null +++ b/backend/src/main/java/es/recursoscatolicos/service/ParroquiaService.java @@ -0,0 +1,31 @@ +package es.recursoscatolicos.service; + +import es.recursoscatolicos.dto.GrupoDTO; +import es.recursoscatolicos.dto.ParroquiaDTO; +import es.recursoscatolicos.repository.GrupoRepository; +import es.recursoscatolicos.repository.ParroquiaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ParroquiaService { + + private final ParroquiaRepository parroquiaRepository; + private final GrupoRepository grupoRepository; + + public List listarTodas() { + return parroquiaRepository.findAll().stream() + .map(ParroquiaDTO::from) + .collect(Collectors.toList()); + } + + public List listarGruposPorParroquia(Long parroquiaId) { + return grupoRepository.findByParroquiaId(parroquiaId).stream() + .map(GrupoDTO::from) + .collect(Collectors.toList()); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..e5e9edb --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,59 @@ +# ================================ +# SERVIDOR +# ================================ +server.port=8080 + +# Para producción con HTTPS, descomentar y configurar el keystore: +# server.ssl.enabled=true +# server.ssl.key-store=classpath:keystore.p12 +# server.ssl.key-store-password=CAMBIAR +# server.ssl.key-store-type=PKCS12 +# server.ssl.key-alias=tomcat + +# ================================ +# BASE DE DATOS (MySQL / MariaDB) +# ================================ +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/recursoscatolicos?useSSL=false&serverTimezone=Europe/Madrid&allowPublicKeyRetrieval=true&characterEncoding=UTF-8} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:rc_user} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# Reintentos de conexión — útiles cuando MySQL tarda en arrancar (Docker) +spring.datasource.hikari.connection-timeout=30000 +spring.datasource.hikari.initialization-fail-timeout=60000 +spring.datasource.hikari.connection-test-query=SELECT 1 + +# ================================ +# JPA / HIBERNATE +# ================================ +# "update" crea las tablas automáticamente. Cambiar a "validate" en producción estable. +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=true +# Hibernate detecta el dialecto automáticamente, no hace falta declararlo +spring.jpa.open-in-view=false + +# ================================ +# JWT +# ================================ +# Secreto de al menos 32 caracteres (256 bits) para HS256. +# CAMBIAR por un valor aleatorio seguro en producción. +jwt.secret=${JWT_SECRET} +jwt.expiration=${JWT_EXPIRATION:86400000} + +# ================================ +# CORS — orígenes permitidos (separados por coma) +# ================================ +cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5500,http://127.0.0.1:5500} + +# ================================ +# ENCODING — forzar UTF-8 en todas las respuestas HTTP +# ================================ +server.servlet.encoding.charset=UTF-8 +server.servlet.encoding.force=true + +# ================================ +# LOGGING +# ================================ +logging.level.es.recursoscatolicos=INFO +logging.level.org.springframework.security=WARN diff --git a/backend/src/main/resources/db/init.sql b/backend/src/main/resources/db/init.sql new file mode 100644 index 0000000..9e616bb --- /dev/null +++ b/backend/src/main/resources/db/init.sql @@ -0,0 +1,43 @@ +-- ============================================================ +-- Base de datos: Recursos Católicos +-- Ejecutar este script una sola vez antes de arrancar la app. +-- La app creará las tablas automáticamente (ddl-auto=update). +-- Este script añade datos de ejemplo. +-- ============================================================ + +-- Crear la base de datos si no existe +CREATE DATABASE IF NOT EXISTS recursoscatolicos + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE recursoscatolicos; + +-- Usuario de la aplicación (ejecutar como root) +-- Cambia 'TU_PASSWORD_SEGURA' por una contraseña real +CREATE USER IF NOT EXISTS 'rc_user'@'localhost' IDENTIFIED BY 'TU_PASSWORD_SEGURA'; +GRANT ALL PRIVILEGES ON recursoscatolicos.* TO 'rc_user'@'localhost'; +FLUSH PRIVILEGES; + +-- ============================================================ +-- DATOS DE EJEMPLO (ejecutar después de arrancar la app +-- por primera vez, para que Hibernate cree las tablas) +-- ============================================================ + +-- Parroquias de ejemplo +INSERT INTO parroquias (nombre, direccion) VALUES + ('San José Obrero', 'Calle Mayor, 12, Madrid'), + ('Nuestra Señora del Pilar', 'Avda. de la Paz, 5, Zaragoza'), + ('Santa María de la Asunción', 'Plaza de la Iglesia, 1, Sevilla') +ON DUPLICATE KEY UPDATE nombre = nombre; + +-- Grupos de ejemplo (asociados a la parroquia 1) +INSERT INTO grupos (nombre, parroquia_id) VALUES + ('Catequesis infantil', 1), + ('Catequesis de adultos', 1), + ('Hogar de Misiones', 1), + ('Cáritas parroquial', 1), + ('Grupo de jóvenes', 1), + ('Catequesis infantil', 2), + ('Grupo de jóvenes', 2), + ('Cáritas parroquial', 3) +ON DUPLICATE KEY UPDATE nombre = nombre; diff --git a/biblioteca-cristiana20260120.html b/biblioteca-cristiana20260120.html deleted file mode 100755 index feeaa4c..0000000 --- a/biblioteca-cristiana20260120.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - Biblioteca cristiana - - - - - -
-

RECURSOS CATÓLICOS

-

Biblioteca cristiana

- -
- Martes, 15 de enero de 2026 -
- - - -
-
-
-

Biblia y evangelio

-

Sagrada Biblia de la conferencia Episcopal Española (CEE)

-

Catecismo de la doctrina cristiana

-
- -
-

Vidas de santos

-

365 días con la madre Teresa de Calcuta

-

San Francisco de Asis (Padre de los Franciscanos)

-

Historia de un alma: La doctrina espiritual de Santa Teresita del Niño Jesús (VersiónKindle)

-
- -
-

Películas

-

Una monja de cuidado (Sister Act)

-

The chosen

-
-
- - \ No newline at end of file diff --git a/biblioteca-cristiana.html b/frontend/biblioteca-cristiana.html old mode 100755 new mode 100644 similarity index 96% rename from biblioteca-cristiana.html rename to frontend/biblioteca-cristiana.html index e71685c..de2f505 --- a/biblioteca-cristiana.html +++ b/frontend/biblioteca-cristiana.html @@ -28,6 +28,7 @@
+ diff --git a/css/biblioteca.css b/frontend/css/biblioteca.css old mode 100755 new mode 100644 similarity index 100% rename from css/biblioteca.css rename to frontend/css/biblioteca.css diff --git a/frontend/css/diario.css b/frontend/css/diario.css new file mode 100644 index 0000000..4686058 --- /dev/null +++ b/frontend/css/diario.css @@ -0,0 +1,346 @@ +/* ================================ + DIARIO DE ORACIÓN + Sigue la paleta de estilos.css +================================ */ + +/* CONTENEDOR PRINCIPAL */ +.contenedor-diario { + max-width: 680px; + margin: 0 auto; + padding: 2rem 1rem 4rem; +} + +/* CABECERA */ +.diario-cabecera { + text-align: center; + margin-bottom: 1.8rem; +} + +.diario-cabecera h2 { + font-family: 'EB Garamond', serif; + font-size: 2rem; + color: var(--body-texto); + margin: 0 0 0.3rem; +} + +.saludo-diario { + font-family: 'EB Garamond', serif; + font-style: italic; + font-size: 1.05rem; + color: var(--body-texto-suave); + margin: 0; +} + +/* ─── NAVEGACIÓN DE FECHA ─── */ +.nav-fecha { + display: flex; + align-items: center; + justify-content: center; + gap: 0.8rem; + margin-bottom: 1.5rem; +} + +.btn-nav { + width: 40px; + height: 40px; + border-radius: 50%; + border: 1px solid var(--body-borde); + background: none; + color: var(--body-texto); + font-size: 1.6rem; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + padding: 0; +} + +.btn-nav:hover:not(:disabled) { + background: var(--body-borde); +} + +.btn-nav:disabled { + opacity: 0.3; + cursor: default; +} + +.fecha-centro { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; + justify-content: center; +} + +.display-fecha { + font-family: 'EB Garamond', serif; + font-size: 1.1rem; + font-style: italic; + color: var(--body-texto); + text-transform: capitalize; + text-align: center; +} + +.input-fecha-oculto { + position: absolute; + opacity: 0; + width: 0; + height: 0; + pointer-events: none; +} + +.btn-cambiar-fecha { + background: none; + border: none; + font-size: 1.1rem; + cursor: pointer; + opacity: 0.55; + transition: opacity 0.2s; + padding: 0; + line-height: 1; +} + +.btn-cambiar-fecha:hover { opacity: 1; } + +/* ─── ESTADOS DE ÁNIMO ─── */ +.estados-animo { + margin-bottom: 1.2rem; + text-align: center; +} + +.label-estados { + font-family: 'EB Garamond', serif; + font-style: italic; + color: var(--body-texto-suave); + margin: 0 0 0.7rem; + font-size: 1rem; +} + +.botones-estados { + display: flex; + justify-content: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.btn-estado { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0.35rem 0.85rem; + border-radius: 50px; + border: 1px solid var(--body-borde); + background: var(--body-fondo); + color: var(--body-texto); + font-family: 'Nunito', sans-serif; + font-size: 0.88rem; + cursor: pointer; + transition: all 0.2s; +} + +.btn-estado:hover { + border-color: var(--color-acento); + background: #faf6ed; +} + +.btn-estado.activo { + background: var(--color-acento); + color: var(--color-primario); + border-color: var(--color-acento); + font-weight: 700; +} + +/* ─── INPUTS ─── */ +.input-titulo { + width: 100%; + box-sizing: border-box; + padding: 0.6rem 0.9rem; + font-family: 'EB Garamond', serif; + font-size: 1.1rem; + border: 1px solid var(--body-borde); + border-radius: 8px; + background: var(--body-fondo); + color: var(--body-texto); + margin-bottom: 0.8rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.textarea-diario { + width: 100%; + box-sizing: border-box; + padding: 0.8rem 0.9rem; + font-family: 'EB Garamond', serif; + font-size: 1.1rem; + line-height: 1.7; + border: 1px solid var(--body-borde); + border-radius: 8px; + background: var(--body-fondo); + color: var(--body-texto); + resize: vertical; + min-height: 180px; + margin-bottom: 1rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.input-titulo:focus, +.textarea-diario:focus { + outline: none; + border-color: var(--color-acento); + box-shadow: 0 0 0 2px rgba(201, 168, 76, 0.25); +} + +/* ─── ACCIONES ─── */ +.acciones-diario { + display: flex; + gap: 1rem; + justify-content: center; +} + +.btn-guardar-diario { + background: var(--color-primario); + color: var(--blanco-puro); +} + +.btn-guardar-diario:hover { + background: var(--color-hover); +} + +.btn-borrar-diario { + background: transparent !important; + color: #b22222 !important; + border: 1px solid #b22222 !important; +} + +.btn-borrar-diario:hover { + background: #b22222 !important; + color: white !important; +} + +/* ─── MENSAJE FEEDBACK ─── */ +.mensaje-diario { + margin-top: 0.8rem; + text-align: center; + padding: 0.5rem 1rem; + border-radius: 8px; + font-size: 0.95rem; +} + +.mensaje-diario.success { background: #e8f5e9; color: #2e7d32; } +.mensaje-diario.error { background: #fce4ec; color: #b71c1c; } +.mensaje-diario.info { background: #e3f2fd; color: #1565c0; } + +/* ─── HISTORIAL ─── */ +.historial-diario h3 { + font-family: 'EB Garamond', serif; + font-size: 1.4rem; + margin: 0 0 1rem; + color: var(--body-texto); +} + +.lista-entradas { + list-style: none; + padding: 0; + margin: 0; +} + +.entrada-item { + display: flex; + align-items: flex-start; + gap: 0.8rem; + padding: 0.75rem 0.8rem; + border-radius: 10px; + cursor: pointer; + transition: background 0.15s; + border-bottom: 1px solid var(--body-borde); +} + +.entrada-item:last-child { border-bottom: none; } + +.entrada-item:hover { background: #faf3e0; } + +.entrada-item.entrada-hoy { + background: #fff9ed; + border-left: 3px solid var(--color-acento); + padding-left: 0.6rem; +} + +.entrada-icono { + font-size: 1.35rem; + flex-shrink: 0; + margin-top: 2px; +} + +.entrada-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.entrada-fecha { + font-size: 0.82rem; + color: var(--body-texto-suave); + font-family: 'Nunito', sans-serif; + text-transform: capitalize; +} + +.entrada-fecha em { + color: var(--color-acento); + font-style: normal; + font-weight: 700; +} + +.entrada-titulo { + font-family: 'EB Garamond', serif; + font-size: 1rem; + font-weight: 600; + color: var(--body-texto); +} + +.entrada-preview { + font-size: 0.88rem; + color: var(--body-texto-suave); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 520px; +} + +/* ─── PREVIEW EN PORTADA ─── */ +.diario-preview-index { + background: rgba(201, 168, 76, 0.08); + border-radius: 10px; + padding: 0.8rem 1rem; + margin: 0.7rem 0 1rem; + border-left: 3px solid var(--color-acento); + text-align: left; + font-family: 'EB Garamond', serif; + font-style: italic; + color: var(--body-texto-suave); + font-size: 1rem; + line-height: 1.5; +} + +.diario-preview-index strong { + display: block; + font-style: normal; + color: var(--body-texto); + margin-bottom: 0.2rem; + font-size: 0.9rem; +} + +/* ─── BLOQUE ÍNTIMO EN PORTADA ─── */ +.bloque-intimo { + background: linear-gradient(135deg, #fdfaf3 0%, #f8f0dc 100%); + border: 1px solid var(--color-acento); +} + +/* ─── RESPONSIVE ─── */ +@media (max-width: 600px) { + .btn-estado span { display: none; } + .btn-estado { font-size: 1.3rem; padding: 0.4rem 0.6rem; } + .acciones-diario { flex-direction: column; align-items: center; } + .display-fecha { font-size: 0.95rem; } +} diff --git a/css/estilos.css b/frontend/css/estilos.css old mode 100755 new mode 100644 similarity index 74% rename from css/estilos.css rename to frontend/css/estilos.css index dec68d8..35145fb --- a/css/estilos.css +++ b/frontend/css/estilos.css @@ -515,17 +515,202 @@ body { background: rgba(255,255,255,0.35); } +/* Botones header sesión — funcionan como y como +
+ + + +
+ + + + +
+

¿Cómo estás hoy en tu interior?

+
+ + + + + +
+
+ + + + + + + + +
+ + +
+ + + + + +
+

📖 Mis entradas

+
    + +
    + + + + + + + + + diff --git a/header.html b/frontend/header.html old mode 100755 new mode 100644 similarity index 96% rename from header.html rename to frontend/header.html index ba94833..416ed73 --- a/header.html +++ b/frontend/header.html @@ -30,6 +30,7 @@
    Rosario Oraciones Básicas Intenciones + Diario Biblioteca cristiana diff --git a/img/biblia.jpg b/frontend/img/biblia.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/biblia.jpg rename to frontend/img/biblia.jpg diff --git a/img/biblioteca.jpg b/frontend/img/biblioteca.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/biblioteca.jpg rename to frontend/img/biblioteca.jpg diff --git a/img/dolorosos1.jpg b/frontend/img/dolorosos1.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/dolorosos1.jpg rename to frontend/img/dolorosos1.jpg diff --git a/img/dolorosos2.jpg b/frontend/img/dolorosos2.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/dolorosos2.jpg rename to frontend/img/dolorosos2.jpg diff --git a/img/dolorosos3.jpg b/frontend/img/dolorosos3.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/dolorosos3.jpg rename to frontend/img/dolorosos3.jpg diff --git a/img/dolorosos4.jpg b/frontend/img/dolorosos4.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/dolorosos4.jpg rename to frontend/img/dolorosos4.jpg diff --git a/img/dolorosos5.jpg b/frontend/img/dolorosos5.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/dolorosos5.jpg rename to frontend/img/dolorosos5.jpg diff --git a/img/favicon.png b/frontend/img/favicon.png old mode 100755 new mode 100644 similarity index 100% rename from img/favicon.png rename to frontend/img/favicon.png diff --git a/img/faviconClaro.png b/frontend/img/faviconClaro.png old mode 100755 new mode 100644 similarity index 100% rename from img/faviconClaro.png rename to frontend/img/faviconClaro.png diff --git a/img/gloriosos1.jpg b/frontend/img/gloriosos1.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gloriosos1.jpg rename to frontend/img/gloriosos1.jpg diff --git a/img/gloriosos2.jpg b/frontend/img/gloriosos2.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gloriosos2.jpg rename to frontend/img/gloriosos2.jpg diff --git a/img/gloriosos3.jpg b/frontend/img/gloriosos3.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gloriosos3.jpg rename to frontend/img/gloriosos3.jpg diff --git a/img/gloriosos4.jpg b/frontend/img/gloriosos4.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gloriosos4.jpg rename to frontend/img/gloriosos4.jpg diff --git a/img/gloriosos5.jpg b/frontend/img/gloriosos5.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gloriosos5.jpg rename to frontend/img/gloriosos5.jpg diff --git a/img/gozosos1.jpg b/frontend/img/gozosos1.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gozosos1.jpg rename to frontend/img/gozosos1.jpg diff --git a/img/gozosos2.jpg b/frontend/img/gozosos2.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gozosos2.jpg rename to frontend/img/gozosos2.jpg diff --git a/img/gozosos3.jpg b/frontend/img/gozosos3.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gozosos3.jpg rename to frontend/img/gozosos3.jpg diff --git a/img/gozosos4.jpg b/frontend/img/gozosos4.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gozosos4.jpg rename to frontend/img/gozosos4.jpg diff --git a/img/gozosos5.jpg b/frontend/img/gozosos5.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/gozosos5.jpg rename to frontend/img/gozosos5.jpg diff --git a/img/iconos/cruz.png b/frontend/img/iconos/cruz.png old mode 100755 new mode 100644 similarity index 100% rename from img/iconos/cruz.png rename to frontend/img/iconos/cruz.png diff --git a/img/iconos/flor.png b/frontend/img/iconos/flor.png old mode 100755 new mode 100644 similarity index 100% rename from img/iconos/flor.png rename to frontend/img/iconos/flor.png diff --git a/img/iconos/vela.png b/frontend/img/iconos/vela.png old mode 100755 new mode 100644 similarity index 100% rename from img/iconos/vela.png rename to frontend/img/iconos/vela.png diff --git a/img/libros/biblia-cee.jpg b/frontend/img/libros/biblia-cee.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/libros/biblia-cee.jpg rename to frontend/img/libros/biblia-cee.jpg diff --git a/img/libros/biblia-cee.png b/frontend/img/libros/biblia-cee.png old mode 100755 new mode 100644 similarity index 100% rename from img/libros/biblia-cee.png rename to frontend/img/libros/biblia-cee.png diff --git a/img/libros/catecismo-astete.jpg b/frontend/img/libros/catecismo-astete.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/libros/catecismo-astete.jpg rename to frontend/img/libros/catecismo-astete.jpg diff --git a/img/libros/catecismo-astete.png b/frontend/img/libros/catecismo-astete.png old mode 100755 new mode 100644 similarity index 100% rename from img/libros/catecismo-astete.png rename to frontend/img/libros/catecismo-astete.png diff --git a/img/luminosos1.jpg b/frontend/img/luminosos1.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/luminosos1.jpg rename to frontend/img/luminosos1.jpg diff --git a/img/luminosos2.jpg b/frontend/img/luminosos2.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/luminosos2.jpg rename to frontend/img/luminosos2.jpg diff --git a/img/luminosos3.jpg b/frontend/img/luminosos3.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/luminosos3.jpg rename to frontend/img/luminosos3.jpg diff --git a/img/luminosos4.jpg b/frontend/img/luminosos4.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/luminosos4.jpg rename to frontend/img/luminosos4.jpg diff --git a/img/luminosos5.jpg b/frontend/img/luminosos5.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/luminosos5.jpg rename to frontend/img/luminosos5.jpg diff --git a/img/oraciones.jpg b/frontend/img/oraciones.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/oraciones.jpg rename to frontend/img/oraciones.jpg diff --git a/img/peliculas.jpg b/frontend/img/peliculas.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/peliculas.jpg rename to frontend/img/peliculas.jpg diff --git a/img/rosario.jpg b/frontend/img/rosario.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/rosario.jpg rename to frontend/img/rosario.jpg diff --git a/img/santos.jpg b/frontend/img/santos.jpg old mode 100755 new mode 100644 similarity index 100% rename from img/santos.jpg rename to frontend/img/santos.jpg diff --git a/index.html b/frontend/index.html old mode 100755 new mode 100644 similarity index 88% rename from index.html rename to frontend/index.html index 513012e..d741692 --- a/index.html +++ b/frontend/index.html @@ -6,6 +6,7 @@ + @@ -35,14 +36,14 @@ - +
    + Escribir en mi diario +
    @@ -79,6 +80,7 @@ + diff --git a/intenciones.html b/frontend/intenciones.html old mode 100755 new mode 100644 similarity index 100% rename from intenciones.html rename to frontend/intenciones.html diff --git a/js/api-config.js b/frontend/js/api-config.js similarity index 95% rename from js/api-config.js rename to frontend/js/api-config.js index 9a29b0a..2335f57 100644 --- a/js/api-config.js +++ b/frontend/js/api-config.js @@ -5,7 +5,7 @@ const API_BASE = ( location.hostname === "127.0.0.1" || location.hostname === "" // file:// abierto directamente ) ? "http://localhost:8080" - : "https://recursos-catolicos.es:8080"; + : "/api"; /** * Realiza una llamada autenticada a la API. diff --git a/frontend/js/auth.js b/frontend/js/auth.js new file mode 100644 index 0000000..7a730ed --- /dev/null +++ b/frontend/js/auth.js @@ -0,0 +1,297 @@ +// ================================ +// UTILIDADES DE AUTENTICACIÓN +// ================================ + +/** Devuelve el token JWT o null si no hay sesión. */ +function getToken() { + return localStorage.getItem("token"); +} + +/** Devuelve el objeto usuario guardado en sesión, o null. */ +function getUsuario() { + const u = localStorage.getItem("usuario"); + return u ? JSON.parse(u) : null; +} + +/** + * Verifica que el usuario esté autenticado. + * Si no lo está, redirige a login.html y devuelve null. + */ +function verificarAuth() { + if (!getToken()) { + window.location.href = "login.html"; + return null; + } + return getUsuario(); +} + +/** Cierra la sesión y recarga la página actual (o va a index si es protegida). */ +function cerrarSesion() { + localStorage.removeItem("token"); + localStorage.removeItem("usuario"); + const paginasProtegidas = ["intenciones.html", "diario-oracion.html"]; + const actual = location.pathname.split("/").pop(); + if (paginasProtegidas.includes(actual)) { + window.location.href = "index.html"; + } else { + location.reload(); + } +} + +/** + * Muestra el nombre del usuario y el botón de cerrar sesión en el header. + * Sin sesión: muestra botones que abren el modal de auth. + */ +function mostrarSesionEnHeader() { + const usuario = getUsuario(); + const contenedor = document.getElementById("header-sesion"); + if (!contenedor) return; + + if (usuario) { + contenedor.innerHTML = ` + 👤 ${usuario.nombre} + + `; + } else { + contenedor.innerHTML = ` + + + `; + } +} + + +// ================================ +// MODAL DE AUTENTICACIÓN +// ================================ + +/** Resuelve la base de la API aunque api-config.js no esté cargado en la página. */ +function _apiBase() { + if (typeof API_BASE !== "undefined") return API_BASE; + return (location.hostname === "localhost" || + location.hostname === "127.0.0.1" || + location.hostname === "") + ? "http://localhost:8080" + : "https://recursos-catolicos.es:8080"; +} + +/** Crea e inserta el modal en el DOM la primera vez que se abre. */ +function _inyectarModalAuth() { + if (document.getElementById("modal-auth")) return; + + const el = document.createElement("div"); + el.id = "modal-auth"; + el.className = "modal-auth-overlay"; + el.setAttribute("role", "dialog"); + el.setAttribute("aria-modal", "true"); + el.setAttribute("aria-label", "Iniciar sesión o registrarse"); + el.innerHTML = ` + + `; + document.body.appendChild(el); + + // — Cerrar — + document.getElementById("modal-auth-cerrar").addEventListener("click", cerrarModalAuth); + el.addEventListener("click", e => { if (e.target === el) cerrarModalAuth(); }); + document.addEventListener("keydown", _modalKeyHandler); + + // — Pestañas — + el.querySelectorAll(".modal-auth-tab").forEach(tab => { + tab.addEventListener("click", () => { + el.querySelectorAll(".modal-auth-tab").forEach(t => t.classList.remove("activa")); + tab.classList.add("activa"); + document.getElementById("modal-panel-login").style.display = tab.dataset.tab === "login" ? "block" : "none"; + document.getElementById("modal-panel-registro").style.display = tab.dataset.tab === "registro" ? "block" : "none"; + _focoModal(tab.dataset.tab); + }); + }); + + // — Enter para enviar — + ["modal-email", "modal-password"].forEach(id => + document.getElementById(id).addEventListener("keydown", e => { if (e.key === "Enter") _loginModal(); }) + ); + ["modal-nombre", "modal-email-reg", "modal-password-reg"].forEach(id => + document.getElementById(id).addEventListener("keydown", e => { if (e.key === "Enter") _registroModal(); }) + ); + + document.getElementById("modal-btn-login").addEventListener("click", _loginModal); + document.getElementById("modal-btn-registro").addEventListener("click", _registroModal); +} + +function _modalKeyHandler(e) { + if (e.key === "Escape") cerrarModalAuth(); +} + +function _focoModal(tab) { + setTimeout(() => { + const id = tab === "login" ? "modal-email" : "modal-nombre"; + document.getElementById(id)?.focus(); + }, 80); +} + +/** Abre el modal en la pestaña indicada ('login' | 'registro'). */ +function abrirModalAuth(tab = "login") { + _inyectarModalAuth(); + const modal = document.getElementById("modal-auth"); + + // Limpiar mensajes y campos al abrir + ["modal-login-msg", "modal-registro-msg"].forEach(id => { + const el = document.getElementById(id); + if (el) { el.textContent = ""; el.className = "modal-auth-msg"; } + }); + + modal.querySelectorAll(".modal-auth-tab").forEach(t => t.classList.remove("activa")); + modal.querySelector(`[data-tab="${tab}"]`).classList.add("activa"); + document.getElementById("modal-panel-login").style.display = tab === "login" ? "block" : "none"; + document.getElementById("modal-panel-registro").style.display = tab === "registro" ? "block" : "none"; + + modal.classList.add("activo"); + document.body.classList.add("modal-abierto"); + _focoModal(tab); +} + +/** Cierra el modal. */ +function cerrarModalAuth() { + const modal = document.getElementById("modal-auth"); + if (!modal) return; + modal.classList.remove("activo"); + document.body.classList.remove("modal-abierto"); +} + +/** Lógica de inicio de sesión desde el modal. */ +async function _loginModal() { + const email = document.getElementById("modal-email").value.trim(); + const password = document.getElementById("modal-password").value.trim(); + const msg = document.getElementById("modal-login-msg"); + const btn = document.getElementById("modal-btn-login"); + + msg.textContent = ""; + msg.className = "modal-auth-msg"; + + if (!email || !password) { + msg.textContent = "Completa todos los campos."; + msg.classList.add("error"); + return; + } + + btn.disabled = true; + btn.textContent = "Entrando…"; + + try { + const res = await fetch(`${_apiBase()}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + if (res.ok) { + const data = await res.json(); + localStorage.setItem("token", data.token); + localStorage.setItem("usuario", JSON.stringify(data.usuario)); + msg.textContent = `¡Bienvenido, ${data.usuario.nombre}! ✝`; + msg.classList.add("success"); + setTimeout(() => location.reload(), 900); + } else { + msg.textContent = "Email o contraseña incorrectos."; + msg.classList.add("error"); + btn.disabled = false; + btn.textContent = "Entrar"; + } + } catch (e) { + msg.textContent = "No se pudo conectar con el servidor."; + msg.classList.add("error"); + btn.disabled = false; + btn.textContent = "Entrar"; + } +} + +/** Lógica de registro desde el modal (individual; registro completo en register.html). */ +async function _registroModal() { + const nombre = document.getElementById("modal-nombre").value.trim(); + const email = document.getElementById("modal-email-reg").value.trim(); + const password = document.getElementById("modal-password-reg").value.trim(); + const msg = document.getElementById("modal-registro-msg"); + const btn = document.getElementById("modal-btn-registro"); + + msg.textContent = ""; + msg.className = "modal-auth-msg"; + + if (!nombre || !email || !password) { + msg.textContent = "Completa todos los campos."; + msg.classList.add("error"); + return; + } + if (password.length < 8) { + msg.textContent = "La contraseña debe tener al menos 8 caracteres."; + msg.classList.add("error"); + return; + } + + btn.disabled = true; + btn.textContent = "Registrando…"; + + try { + const res = await fetch(`${_apiBase()}/auth/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ nombre, email, password, tipoUsuario: "individual" }) + }); + + if (res.ok) { + msg.textContent = "¡Cuenta creada! Iniciando sesión…"; + msg.classList.add("success"); + + // Auto-login tras el registro + const loginRes = await fetch(`${_apiBase()}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + if (loginRes.ok) { + const data = await loginRes.json(); + localStorage.setItem("token", data.token); + localStorage.setItem("usuario", JSON.stringify(data.usuario)); + } + setTimeout(() => location.reload(), 900); + } else { + const error = await res.text(); + msg.textContent = error || "No se pudo crear la cuenta."; + msg.classList.add("error"); + btn.disabled = false; + btn.textContent = "Registrarme"; + } + } catch (e) { + msg.textContent = "No se pudo conectar con el servidor."; + msg.classList.add("error"); + btn.disabled = false; + btn.textContent = "Registrarme"; + } +} diff --git a/js/biblioteca.js b/frontend/js/biblioteca.js old mode 100755 new mode 100644 similarity index 100% rename from js/biblioteca.js rename to frontend/js/biblioteca.js diff --git a/js/codigo.js b/frontend/js/codigo.js old mode 100755 new mode 100644 similarity index 85% rename from js/codigo.js rename to frontend/js/codigo.js index 2dae0ad..d32fcce --- a/js/codigo.js +++ b/frontend/js/codigo.js @@ -8,6 +8,7 @@ document.addEventListener("DOMContentLoaded", () => { visualizarRosario(); recordatorioDifuntos(); cargarIntencionesIndex(); + personalizarSeccionDiario(); }); @@ -309,3 +310,51 @@ async function cargarIntencionesIndex() { .map(i => `
  • ${i.texto}
  • `) .join(""); } + + + +/* ============================================ + PERSONALIZACIÓN SECCIÓN DIARIO EN PORTADA +============================================ */ + +function personalizarSeccionDiario() { + const previewElem = document.getElementById('diario-preview-index'); + const textoElem = document.getElementById('texto-diario-index'); + const botonElem = document.getElementById('boton-diario-index'); + if (!previewElem) return; + + const usuario = typeof getUsuario === 'function' ? getUsuario() : null; + if (!usuario) return; // sin sesión: se muestra el texto genérico + + // Ajustar texto intro con el nombre + if (textoElem) { + textoElem.textContent = `Tu espacio personal, ${usuario.nombre}.`; + } + + // Buscar entrada de hoy en localStorage + const hoy = (() => { + const d = new Date(); + const offset = d.getTimezoneOffset() * 60000; + return new Date(d - offset).toISOString().split('T')[0]; + })(); + + let entradas = {}; + try { + const data = localStorage.getItem(`diario_${usuario.id}`); + if (data) entradas = JSON.parse(data); + } catch (e) { /* sin entradas */ } + + const entradaHoy = entradas[hoy] || null; + + if (entradaHoy && entradaHoy.texto) { + const icono = { paz: '🕊️', gratitud: '🙏', lucha: '😔', gozo: '✨', silencio: '🌿' }[entradaHoy.estado] || '🕯'; + const preview = entradaHoy.texto.substring(0, 120) + (entradaHoy.texto.length > 120 ? '…' : ''); + previewElem.innerHTML = ` +
    + ${icono} ${entradaHoy.titulo || 'Mi oración de hoy'} + ${preview} +
    + `; + if (botonElem) botonElem.textContent = 'Continuar escribiendo'; + } +} diff --git a/frontend/js/diario.js b/frontend/js/diario.js new file mode 100644 index 0000000..505feb7 --- /dev/null +++ b/frontend/js/diario.js @@ -0,0 +1,310 @@ +// ================================ +// DIARIO DE ORACIÓN +// Persistencia en API (con fallback a localStorage si no hay conexión). +// Requiere: api-config.js, auth.js +// ================================ + +const ESTADOS_DIARIO = { + paz: { icono: '🕊️', label: 'Paz' }, + gratitud: { icono: '🙏', label: 'Gratitud' }, + lucha: { icono: '😔', label: 'Lucha' }, + gozo: { icono: '✨', label: 'Gozo' }, + silencio: { icono: '🌿', label: 'Silencio' } +}; + +let _usuario = null; +let _fechaSeleccionada = null; + +// Cache en memoria para evitar peticiones redundantes durante la sesión. +const _cache = {}; + +// ── INICIALIZACIÓN ────────────────────────────────────────── + +document.addEventListener("DOMContentLoaded", () => { + _usuario = verificarAuth(); + if (!_usuario) return; + + const hoy = new Date(); + _fechaSeleccionada = toFechaISO(hoy); + + document.getElementById('saludo-usuario').textContent = saludoPersonal(_usuario.nombre, hoy); + document.getElementById('fecha-entrada').value = _fechaSeleccionada; + + cargarEntrada(_fechaSeleccionada); + cargarListaEntradas(); + + document.getElementById('btn-guardar-entrada').addEventListener('click', guardarEntrada); + document.getElementById('btn-borrar-entrada').addEventListener('click', borrarEntrada); + document.getElementById('btn-anterior').addEventListener('click', () => navegarFecha(-1)); + document.getElementById('btn-siguiente').addEventListener('click', () => navegarFecha(1)); + + document.getElementById('fecha-entrada').addEventListener('change', e => { + _fechaSeleccionada = e.target.value; + cargarEntrada(_fechaSeleccionada); + }); + + document.querySelectorAll('.btn-estado').forEach(btn => { + btn.addEventListener('click', () => seleccionarEstado(btn.dataset.estado)); + }); +}); + +// ── SALUDO ────────────────────────────────────────────────── + +function saludoPersonal(nombre, fecha) { + const h = fecha.getHours(); + const franja = h < 13 ? 'Buenos días' : h < 20 ? 'Buenas tardes' : 'Buenas noches'; + return `${franja}, ${nombre}. Un momento de silencio contigo.`; +} + +// ── API ───────────────────────────────────────────────────── + +async function _apiGetEntrada(fecha) { + try { + const res = await apiCall(`/diario/${fecha}`); + if (!res) return null; + if (res.status === 404) return null; + if (!res.ok) throw new Error('Error al cargar entrada'); + return await res.json(); + } catch (e) { + return _localGetEntrada(fecha); + } +} + +async function _apiGetTodas() { + try { + const res = await apiCall('/diario'); + if (!res || !res.ok) throw new Error('Error al cargar entradas'); + return await res.json(); + } catch (e) { + return _localGetTodas(); + } +} + +async function _apiGuardar(fecha, titulo, texto, estado) { + try { + const res = await apiCall('/diario', { + method: 'POST', + body: JSON.stringify({ fecha, titulo, texto, estado }) + }); + if (!res || !res.ok) throw new Error('Error al guardar'); + const dto = await res.json(); + _localSetEntrada(fecha, { id: dto.id, fecha, titulo, texto, estado }); + return dto; + } catch (e) { + _localSetEntrada(fecha, { fecha, titulo, texto, estado }); + return { fecha, titulo, texto, estado }; + } +} + +async function _apiEliminar(fecha) { + const entrada = _cache[fecha] || _localGetEntrada(fecha); + if (entrada?.id) { + try { + await apiCall(`/diario/${entrada.id}`, { method: 'DELETE' }); + } catch (e) { /* sin conexión: eliminar solo local */ } + } + _localEliminarEntrada(fecha); + delete _cache[fecha]; +} + +// ── LOCALSTORAGE (fallback / caché offline) ───────────────── + +function _lsKey() { return `diario_${_usuario.id}`; } + +function _localGetTodas() { + try { + const data = localStorage.getItem(_lsKey()); + const obj = data ? JSON.parse(data) : {}; + return Object.values(obj); + } catch (e) { return []; } +} + +function _localGetEntrada(fecha) { + try { + const data = localStorage.getItem(_lsKey()); + const obj = data ? JSON.parse(data) : {}; + return obj[fecha] || null; + } catch (e) { return null; } +} + +function _localSetEntrada(fecha, entrada) { + try { + const data = localStorage.getItem(_lsKey()); + const obj = data ? JSON.parse(data) : {}; + obj[fecha] = entrada; + localStorage.setItem(_lsKey(), JSON.stringify(obj)); + } catch (e) { /* sin espacio */ } +} + +function _localEliminarEntrada(fecha) { + try { + const data = localStorage.getItem(_lsKey()); + const obj = data ? JSON.parse(data) : {}; + delete obj[fecha]; + localStorage.setItem(_lsKey(), JSON.stringify(obj)); + } catch (e) { /* noop */ } +} + +// ── CARGAR ENTRADA ────────────────────────────────────────── + +async function cargarEntrada(fecha) { + actualizarFechaDisplay(fecha); + + document.getElementById('titulo-entrada').value = ''; + document.getElementById('texto-entrada').value = ''; + document.querySelectorAll('.btn-estado').forEach(b => b.classList.remove('activo')); + document.getElementById('btn-borrar-entrada').style.display = 'none'; + + const entrada = await _apiGetEntrada(fecha); + _cache[fecha] = entrada; + + if (entrada) { + document.getElementById('titulo-entrada').value = entrada.titulo || ''; + document.getElementById('texto-entrada').value = entrada.texto || ''; + if (entrada.estado) { + const btn = document.querySelector(`.btn-estado[data-estado="${entrada.estado}"]`); + if (btn) btn.classList.add('activo'); + } + document.getElementById('btn-borrar-entrada').style.display = 'inline-block'; + } +} + +// ── GUARDAR ───────────────────────────────────────────────── + +async function guardarEntrada() { + const titulo = document.getElementById('titulo-entrada').value.trim(); + const texto = document.getElementById('texto-entrada').value.trim(); + const estado = getEstadoSeleccionado(); + const btn = document.getElementById('btn-guardar-entrada'); + + if (!texto) { + mostrarMensajeDiario('Escribe algo antes de guardar 🙏', 'error'); + return; + } + + btn.disabled = true; + btn.textContent = 'Guardando…'; + + const dto = await _apiGuardar(_fechaSeleccionada, titulo, texto, estado); + _cache[_fechaSeleccionada] = dto; + + btn.disabled = false; + btn.textContent = 'Guardar ✝'; + document.getElementById('btn-borrar-entrada').style.display = 'inline-block'; + + cargarListaEntradas(); + mostrarMensajeDiario('Entrada guardada ✝', 'success'); +} + +// ── BORRAR ─────────────────────────────────────────────────── + +async function borrarEntrada() { + if (!confirm('¿Eliminar esta entrada del diario?')) return; + + await _apiEliminar(_fechaSeleccionada); + + document.getElementById('titulo-entrada').value = ''; + document.getElementById('texto-entrada').value = ''; + document.querySelectorAll('.btn-estado').forEach(b => b.classList.remove('activo')); + document.getElementById('btn-borrar-entrada').style.display = 'none'; + + cargarListaEntradas(); + mostrarMensajeDiario('Entrada eliminada', 'info'); +} + +// ── ESTADOS DE ÁNIMO ──────────────────────────────────────── + +function seleccionarEstado(estado) { + const btn = document.querySelector(`.btn-estado[data-estado="${estado}"]`); + if (!btn) return; + const yaActivo = btn.classList.contains('activo'); + document.querySelectorAll('.btn-estado').forEach(b => b.classList.remove('activo')); + if (!yaActivo) btn.classList.add('activo'); +} + +function getEstadoSeleccionado() { + const activo = document.querySelector('.btn-estado.activo'); + return activo ? activo.dataset.estado : null; +} + +// ── NAVEGACIÓN ─────────────────────────────────────────────── + +function navegarFecha(delta) { + const d = new Date(_fechaSeleccionada + 'T12:00:00'); + d.setDate(d.getDate() + delta); + _fechaSeleccionada = toFechaISO(d); + document.getElementById('fecha-entrada').value = _fechaSeleccionada; + cargarEntrada(_fechaSeleccionada); +} + +function actualizarFechaDisplay(fecha) { + const d = new Date(fecha + 'T12:00:00'); + const opts = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; + document.getElementById('display-fecha').textContent = d.toLocaleDateString('es-ES', opts); + const hoy = toFechaISO(new Date()); + document.getElementById('btn-siguiente').disabled = fecha >= hoy; +} + +// ── LISTA DE ENTRADAS ─────────────────────────────────────── + +async function cargarListaEntradas() { + const lista = document.getElementById('lista-entradas'); + const sinElem = document.getElementById('sin-entradas'); + + const entradas = await _apiGetTodas(); + + const lista_sorted = entradas + .filter(e => e && e.fecha) + .sort((a, b) => b.fecha.localeCompare(a.fecha)); + + if (lista_sorted.length === 0) { + lista.innerHTML = ''; + sinElem.style.display = 'block'; + return; + } + + sinElem.style.display = 'none'; + const hoy = toFechaISO(new Date()); + + lista.innerHTML = lista_sorted.map(e => { + const fecha = e.fecha; + const d = new Date(fecha + 'T12:00:00'); + const fechaStr = d.toLocaleDateString('es-ES', { weekday: 'short', day: 'numeric', month: 'long' }); + const icono = ESTADOS_DIARIO[e.estado]?.icono || '🕯'; + const preview = (e.texto || '').substring(0, 80) + ((e.texto || '').length > 80 ? '…' : ''); + const esHoy = fecha === hoy; + + return ` +
  • + ${icono} +
    + ${fechaStr}${esHoy ? ' (hoy)' : ''} + ${e.titulo ? `${e.titulo}` : ''} + ${preview} +
    +
  • + `; + }).join(''); +} + +function seleccionarFechaLista(fecha) { + _fechaSeleccionada = fecha; + document.getElementById('fecha-entrada').value = fecha; + cargarEntrada(fecha); + document.getElementById('editor-diario').scrollIntoView({ behavior: 'smooth' }); +} + +// ── UTILIDADES ─────────────────────────────────────────────── + +function toFechaISO(date) { + const offset = date.getTimezoneOffset() * 60000; + return new Date(date - offset).toISOString().split('T')[0]; +} + +function mostrarMensajeDiario(texto, tipo) { + const msg = document.getElementById('mensaje-diario'); + msg.textContent = texto; + msg.className = `mensaje-diario ${tipo}`; + msg.style.display = 'block'; + setTimeout(() => { msg.style.display = 'none'; }, 3000); +} \ No newline at end of file diff --git a/js/header.js b/frontend/js/header.js old mode 100755 new mode 100644 similarity index 100% rename from js/header.js rename to frontend/js/header.js diff --git a/js/intenciones.js b/frontend/js/intenciones.js old mode 100755 new mode 100644 similarity index 83% rename from js/intenciones.js rename to frontend/js/intenciones.js index 666d00f..d5051dc --- a/js/intenciones.js +++ b/frontend/js/intenciones.js @@ -205,25 +205,54 @@ function crearHexagono(intencion) { // ── DIFUNTOS ───────────────────────────────────────────────── +// Caché en memoria para evitar peticiones redundantes +let _difuntosCache = null; + +async function _apiGetDifuntos() { + try { + const res = await apiCall('/difuntos/personales'); + if (!res || !res.ok) throw new Error('Error al cargar difuntos'); + const data = await res.json(); + _difuntosCache = data; + // Sincronizar localStorage como caché offline + localStorage.setItem(keyDifuntos(), JSON.stringify(data)); + return data; + } catch (e) { + // Fallback a localStorage si no hay conexión + return JSON.parse(localStorage.getItem(keyDifuntos()) || '[]'); + } +} + // La clave incluye el id del usuario para que cada cuenta tenga sus propios difuntos function keyDifuntos() { - return `difuntos_personales_${usuario ? usuario.id : "anonimo"}`; + return `difuntos_personales_${usuario ? usuario.id : 'anonimo'}`; } -function obtenerDifuntos() { - return JSON.parse(localStorage.getItem(keyDifuntos()) || "[]"); -} - -function agregarDifunto() { +async function agregarDifunto() { const nombre = document.getElementById("difunto-nombre").value.trim(); if (!nombre) return; const nacimiento = document.getElementById("difunto-nacimiento").value || null; const defuncion = document.getElementById("difunto-defuncion").value || null; - const difuntos = obtenerDifuntos(); - difuntos.push({ id: Date.now().toString(), nombre, nacimiento, defuncion }); - localStorage.setItem(keyDifuntos(), JSON.stringify(difuntos)); + try { + const res = await apiCall('/difuntos/personales', { + method: 'POST', + body: JSON.stringify({ nombre, nacimiento, defuncion }) + }); + if (res && res.ok) { + _difuntosCache = null; // invalidar caché + } else { + // Guardar local si falla + const difuntos = JSON.parse(localStorage.getItem(keyDifuntos()) || '[]'); + difuntos.push({ id: Date.now().toString(), nombre, nacimiento, defuncion }); + localStorage.setItem(keyDifuntos(), JSON.stringify(difuntos)); + } + } catch (e) { + const difuntos = JSON.parse(localStorage.getItem(keyDifuntos()) || '[]'); + difuntos.push({ id: Date.now().toString(), nombre, nacimiento, defuncion }); + localStorage.setItem(keyDifuntos(), JSON.stringify(difuntos)); + } document.getElementById("difunto-nombre").value = ""; document.getElementById("difunto-nacimiento").value = ""; @@ -232,15 +261,26 @@ function agregarDifunto() { cargarDifuntos(); } -function eliminarDifunto(id) { - const difuntos = obtenerDifuntos().filter(d => d.id !== id); +async function eliminarDifunto(id) { + try { + const res = await apiCall(`/difuntos/personales/${id}`, { method: 'DELETE' }); + if (res && (res.ok || res.status === 204)) { + _difuntosCache = null; + // Sincronizar localStorage + const local = JSON.parse(localStorage.getItem(keyDifuntos()) || '[]'); + localStorage.setItem(keyDifuntos(), JSON.stringify(local.filter(d => String(d.id) !== String(id)))); + return; + } + } catch (e) { /* sin conexión: eliminar solo local */ } + // Fallback: solo localStorage + const difuntos = JSON.parse(localStorage.getItem(keyDifuntos()) || '[]').filter(d => String(d.id) !== String(id)); localStorage.setItem(keyDifuntos(), JSON.stringify(difuntos)); } -function cargarDifuntos() { +async function cargarDifuntos() { const lista = document.getElementById("lista-difuntos"); const sinDifuntos = document.getElementById("sin-difuntos"); - const difuntos = obtenerDifuntos(); + const difuntos = _difuntosCache || await _apiGetDifuntos(); lista.innerHTML = ""; @@ -275,8 +315,8 @@ function cargarDifuntos() { }); lista.querySelectorAll(".btn-eliminar-difunto").forEach(btn => { - btn.addEventListener("click", () => { - eliminarDifunto(btn.dataset.id); + btn.addEventListener("click", async () => { + await eliminarDifunto(btn.dataset.id); cargarDifuntos(); }); }); diff --git a/js/login.js b/frontend/js/login.js old mode 100755 new mode 100644 similarity index 100% rename from js/login.js rename to frontend/js/login.js diff --git a/js/register.js b/frontend/js/register.js old mode 100755 new mode 100644 similarity index 100% rename from js/register.js rename to frontend/js/register.js diff --git a/js/rosario.js b/frontend/js/rosario.js old mode 100755 new mode 100644 similarity index 100% rename from js/rosario.js rename to frontend/js/rosario.js diff --git a/js/rosario20260118.js b/frontend/js/rosario20260118.js old mode 100755 new mode 100644 similarity index 100% rename from js/rosario20260118.js rename to frontend/js/rosario20260118.js diff --git a/login.html b/frontend/login.html old mode 100755 new mode 100644 similarity index 100% rename from login.html rename to frontend/login.html diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..fc4e19e --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Proxy llamadas a la API hacia el backend Spring Boot + location /api/ { + proxy_pass http://recursos-catolicos-api:8080/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Servir ficheros estáticos + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/oraciones-basicas.html b/frontend/oraciones-basicas.html old mode 100755 new mode 100644 similarity index 99% rename from oraciones-basicas.html rename to frontend/oraciones-basicas.html index c90bc63..197c3c0 --- a/oraciones-basicas.html +++ b/frontend/oraciones-basicas.html @@ -69,6 +69,7 @@ + diff --git a/register.html b/frontend/register.html old mode 100755 new mode 100644 similarity index 100% rename from register.html rename to frontend/register.html diff --git a/rosario.html b/frontend/rosario.html old mode 100755 new mode 100644 similarity index 95% rename from rosario.html rename to frontend/rosario.html index 2a9f6fc..617e7c9 --- a/rosario.html +++ b/frontend/rosario.html @@ -20,6 +20,7 @@ Rosario Image
    + diff --git a/js/auth.js b/js/auth.js deleted file mode 100644 index 359a8df..0000000 --- a/js/auth.js +++ /dev/null @@ -1,54 +0,0 @@ -// ================================ -// UTILIDADES DE AUTENTICACIÓN -// ================================ - -/** Devuelve el token JWT o null si no hay sesión. */ -function getToken() { - return localStorage.getItem("token"); -} - -/** Devuelve el objeto usuario guardado en sesión, o null. */ -function getUsuario() { - const u = localStorage.getItem("usuario"); - return u ? JSON.parse(u) : null; -} - -/** - * Verifica que el usuario esté autenticado. - * Si no lo está, redirige a login.html y devuelve null. - */ -function verificarAuth() { - if (!getToken()) { - window.location.href = "login.html"; - return null; - } - return getUsuario(); -} - -/** Cierra la sesión eliminando los datos locales y redirige al login. */ -function cerrarSesion() { - localStorage.removeItem("token"); - localStorage.removeItem("usuario"); - window.location.href = "login.html"; -} - -/** - * Muestra el nombre del usuario y el botón de cerrar sesión en el header. - * Llama a esta función después de cargar el header. - */ -function mostrarSesionEnHeader() { - const usuario = getUsuario(); - const contenedor = document.getElementById("header-sesion"); - if (!contenedor) return; - - if (usuario) { - contenedor.innerHTML = ` - 👤 ${usuario.nombre} - - `; - } else { - contenedor.innerHTML = ` - Iniciar sesión - `; - } -} diff --git a/prueba.html b/prueba.html deleted file mode 100644 index 293e80d..0000000 --- a/prueba.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Document - - -

    Hola Mundo

    -

    Esta es una página de prueba.

    -

    ¡Bienvenido a la programación web!

    -

    Espero que disfrutes aprendiendo HTML.

    - - \ No newline at end of file