frontend u backend

This commit is contained in:
Tatiana Villa Ema 2026-04-27 21:42:42 +02:00
parent e9ecbcd3d6
commit 823ecac8c0
129 changed files with 3277 additions and 143 deletions

7
backend/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
target/
.git/
.gitignore
.env
*.md
src/test/
docker/

19
backend/.env.example Normal file
View File

@ -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

16
backend/.gitignore vendored Normal file
View File

@ -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/

10
backend/.mvn/jvm.config Normal file
View File

@ -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

34
backend/Dockerfile Normal file
View File

@ -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"]

View File

@ -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:

View File

@ -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);

136
backend/pom.xml Normal file
View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.14</version>
<relativePath/>
</parent>
<groupId>es.recursoscatolicos</groupId>
<artifactId>recursos-catolicos-api</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Recursos Católicos API</name>
<description>Backend para la web de recursos católicos</description>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Validación -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT (jjwt 0.12.x) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=java.base/java.lang=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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");
}
}
}

View File

@ -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<List<EntradaDiarioDTO>> 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<EntradaDiarioDTO> 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();
}
}
}

View File

@ -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<List<DifuntoPersonalDTO>> 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();
}
}
}

View File

@ -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<List<IntencionDTO>> 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<List<IntencionDTO>> 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<List<IntencionDTO>> 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();
}
}
}

View File

@ -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<List<ParroquiaDTO>> listarTodas() {
return ResponseEntity.ok(parroquiaService.listarTodas());
}
@GetMapping("/{id}/grupos")
public ResponseEntity<List<GrupoDTO>> listarGrupos(@PathVariable Long id) {
return ResponseEntity.ok(parroquiaService.listarGruposPorParroquia(id));
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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<GrupoDTO> 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;
}
}

View File

@ -0,0 +1,7 @@
package es.recursoscatolicos.model;
public enum Ambito {
PERSONAL,
PARROQUIA,
GRUPO
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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<Grupo> grupos = new ArrayList<>();
// UserDetails
@Override
public Collection<? extends GrantedAuthority> 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; }
}

View File

@ -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<DifuntoPersonal, Long> {
List<DifuntoPersonal> findByUsuarioIdOrderByNombreAsc(Long usuarioId);
}

View File

@ -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<EntradaDiario, Long> {
List<EntradaDiario> findByUsuarioIdOrderByFechaDesc(Long usuarioId);
Optional<EntradaDiario> findByUsuarioIdAndFecha(Long usuarioId, LocalDate fecha);
}

View File

@ -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<Grupo, Long> {
List<Grupo> findByParroquiaId(Long parroquiaId);
}

View File

@ -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<Intencion, Long> {
List<Intencion> findByUsuarioIdAndAmbito(Long usuarioId, Ambito ambito);
List<Intencion> findByParroquiaIdAndAmbito(Long parroquiaId, Ambito ambito);
List<Intencion> findByGrupoIdAndAmbito(Long grupoId, Ambito ambito);
}

View File

@ -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<Parroquia, Long> {
}

View File

@ -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<Usuario, Long> {
Optional<Usuario> findByEmail(String email);
boolean existsByEmail(String email);
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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<Grupo> 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));
}
}

View File

@ -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<EntradaDiarioDTO> 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);
}
}

View File

@ -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<DifuntoPersonalDTO> 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);
}
}

View File

@ -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<IntencionDTO> getPersonales(Long usuarioId) {
return intencionRepository.findByUsuarioIdAndAmbito(usuarioId, Ambito.PERSONAL)
.stream().map(IntencionDTO::from).collect(Collectors.toList());
}
public List<IntencionDTO> getDeParroquia(Long parroquiaId) {
return intencionRepository.findByParroquiaIdAndAmbito(parroquiaId, Ambito.PARROQUIA)
.stream().map(IntencionDTO::from).collect(Collectors.toList());
}
public List<IntencionDTO> 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);
}
}

View File

@ -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> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claimsResolver.apply(claims);
}
}

View File

@ -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<ParroquiaDTO> listarTodas() {
return parroquiaRepository.findAll().stream()
.map(ParroquiaDTO::from)
.collect(Collectors.toList());
}
public List<GrupoDTO> listarGruposPorParroquia(Long parroquiaId) {
return grupoRepository.findByParroquiaId(parroquiaId).stream()
.map(GrupoDTO::from)
.collect(Collectors.toList());
}
}

View File

@ -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

View File

@ -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;

View File

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Biblioteca cristiana</title>
<link rel="icon" type="image/x-icon" href="img/favicon.png">
<link rel="stylesheet" href="css/estilos.css">
<link rel="stylesheet" href="css/biblioteca-cristiana.css">
</head>
<body>
<header class="header-hoy" id="header-hoy">
<h1 class="titulo">RECURSOS CATÓLICOS</h1>
<h2>Biblioteca cristiana</h2>
<div class="fecha">
<span id="fecha-hoy">Martes, 15 de enero de 2026</span>
</div>
<!-- MENU -->
<nav class="menu-principal">
<a href="index.html">Inicio</a>
<a href="rosario.html">Rosario</a>
<a href="oraciones-basicas.html">Oraciones Básicas</a>
<a href="intenciones.html">Intenciones</a>
<a href="biblioteca-cristiana.html">Biblioteca cristiana</a>
</nav>
</header>
<main class="contenedor">
<section class="bloque bloque-fondo" style="background-image: url('img/biblia.jpg');">
<h3>Biblia y evangelio</h3>
<p><a href="https://www.amazon.es/Sagrada-Bibliapopular-geltexbac-EDICIONES-B%C3%8DBLICAS/dp/8422015617/ref=sr_1_6?__mk_es_ES=%C3%85M%C3%85%C5%BD%C3%95%C3%91&amp;crid=2X17ZIW20JHVF&amp;keywords=biblia&amp;qid=1686516688&amp;sprefix=biblia%2Caps%2C94&amp;sr=8-6">Sagrada Biblia de la conferencia Episcopal Española (CEE)</a></p>
<p><a href="https://www.amazon.es/Catecismo-Doctrina-Cristiana-Ediciones-Populares/dp/8483531496/ref=sr_1_3?crid=1CHRQQCBSKTOY&amp;keywords=catecismo+astete&amp;qid=1686517828&amp;s=books&amp;sprefix=Catecismo+as%2Cstripbooks%2C80&amp;sr=1-3" target="_blank" rel="noreferrer noopener">Catecismo de la doctrina cristiana</a></p>
</section>
<section class="bloque bloque-fondo" style="background-image: url('img/santos.jpg');">
<h3>Vidas de santos</h3>
<p><a href="https://www.amazon.es/d%C3%ADas-Madre-Teresa-Jos%C3%A9-Gonz%C3%A1lez-Balado-ebook/dp/B09SGHLP6R/ref=tmm_kin_swatch_0?_encoding=UTF8&amp;qid=1686518245&amp;sr=1-12">365 días con la madre Teresa de Calcuta</a></p>
<p><a href="https://www.amazon.es/Francisco-Asis-Santos-Amigos-Dios/dp/8484079244/ref=sr_1_1?__mk_es_ES=%C3%85M%C3%85%C5%BD%C3%95%C3%91&amp;crid=3JXXXQXP05Y9H&amp;keywords=san+francisco+de+asis+luis+perez+simon&amp;qid=1686518697&amp;s=digital-text&amp;sprefix=san+francisco+de+asis+luis+perez+simon%2Cdigital-text%2C77&amp;sr=1-1">San Francisco de Asis (Padre de los Franciscanos)</a> </p>
<p><a href="https://amzn.eu/d/0bv8Tit">Historia de un alma: La doctrina espiritual de Santa Teresita del Niño Jesús (VersiónKindle)</a></p>
</section>
<section class="bloque bloque-fondo" style="background-image: url('img/peliculas.jpg');">
<h3>Películas</h3>
<p><a href="https://www.primevideo.com/detail/0RIG0ODHPZZNTT1XF4Y8I9VM46/ref=dvm_src_ret_es_xx_s">Una monja de cuidado</a> (Sister Act)</p>
<p><a href="#">The chosen</a></p>
</section>
</main>
</body>
</html>

View File

@ -28,6 +28,7 @@
<section id="booksContainer" class="grid"></section>
<script src="js/api-config.js"></script>
<script src="js/auth.js"></script>
<script src="js/header.js"></script>
<script src="js/biblioteca.js"></script>

0
css/biblioteca.css → frontend/css/biblioteca.css Executable file → Normal file
View File

346
frontend/css/diario.css Normal file
View File

@ -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; }
}

193
css/estilos.css → frontend/css/estilos.css Executable file → Normal file
View File

@ -515,17 +515,202 @@ body {
background: rgba(255,255,255,0.35);
}
/* Botones header sesión — funcionan como <a> y como <button> */
.btn-sesion,
.btn-registro {
display: inline-block;
font-family: 'Nunito', sans-serif;
border-radius: 15px;
font-size: 0.8rem;
cursor: pointer;
text-decoration: none;
line-height: 1;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.btn-sesion {
padding: 0.3rem 0.8rem;
background: rgba(255,255,255,0.15);
color: white;
border: 1px solid rgba(255,255,255,0.4);
border-radius: 15px;
font-size: 0.8rem;
text-decoration: none;
transition: background 0.2s;
}
.btn-sesion:hover {
background: rgba(255,255,255,0.3);
}
.btn-registro {
padding: 0.3rem 0.9rem;
background: var(--color-acento);
color: var(--color-fondo);
border: 1px solid var(--color-acento);
font-weight: 700;
}
.btn-registro:hover {
background: white;
color: var(--color-fondo);
border-color: white;
}
/* ================================
MODAL DE AUTENTICACIÓN
================================ */
body.modal-abierto { overflow: hidden; }
.modal-auth-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
align-items: center;
justify-content: center;
backdrop-filter: blur(3px);
padding: 1rem;
}
.modal-auth-overlay.activo {
display: flex;
animation: modalFadeIn 0.2s ease;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: translateY(-12px); }
to { opacity: 1; transform: translateY(0); }
}
.modal-auth-caja {
background: #0D1B2E;
border: 1px solid #2D4A7A;
border-radius: 14px;
padding: 2rem 2rem 1.6rem;
width: 100%;
max-width: 380px;
position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
color: #D8E4F5;
}
.modal-auth-cerrar {
position: absolute;
top: 0.7rem;
right: 0.9rem;
background: none;
border: none;
color: #8BAAD4;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.modal-auth-cerrar:hover { color: white; }
/* Pestañas */
.modal-auth-tabs {
display: flex;
gap: 0;
margin-bottom: 1.4rem;
border-bottom: 1px solid #2D4A7A;
}
.modal-auth-tab {
flex: 1;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: #8BAAD4;
font-family: 'Nunito', sans-serif;
font-size: 0.95rem;
font-weight: 600;
padding: 0.5rem 0;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
margin-bottom: -1px;
}
.modal-auth-tab.activa {
color: #C9A84C;
border-bottom-color: #C9A84C;
}
.modal-auth-tab:hover:not(.activa) { color: #D8E4F5; }
/* Panel */
.modal-auth-bienvenida {
font-family: 'EB Garamond', serif;
font-style: italic;
font-size: 1rem;
color: #8BAAD4;
margin: 0 0 1rem;
text-align: center;
}
.modal-auth-panel input {
display: block;
width: 100%;
box-sizing: border-box;
background: #152540;
border: 1px solid #2D4A7A;
border-radius: 8px;
color: #D8E4F5;
font-family: 'Nunito', sans-serif;
font-size: 0.95rem;
padding: 0.6rem 0.85rem;
margin-bottom: 0.8rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.modal-auth-panel input:focus {
outline: none;
border-color: #C9A84C;
box-shadow: 0 0 0 2px rgba(201,168,76,0.2);
}
.modal-auth-btn {
display: block;
width: 100%;
padding: 0.65rem;
background: #C9A84C;
color: #0D1B2E;
border: none;
border-radius: 8px;
font-family: 'Nunito', sans-serif;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
margin-top: 0.3rem;
transition: background 0.2s, opacity 0.2s;
}
.modal-auth-btn:hover:not(:disabled) { background: #e0bb5a; }
.modal-auth-btn:disabled { opacity: 0.6; cursor: default; }
/* Mensaje feedback */
.modal-auth-msg {
min-height: 1.2rem;
text-align: center;
font-size: 0.88rem;
margin: 0.6rem 0 0;
border-radius: 6px;
padding: 0;
}
.modal-auth-msg.error { color: #f08080; }
.modal-auth-msg.success { color: #7ec89c; }
/* Pie del panel */
.modal-auth-pie {
text-align: center;
font-size: 0.8rem;
color: #8BAAD4;
margin: 0.8rem 0 0;
}
.modal-auth-pie a { color: #C9A84C; text-decoration: none; }
.modal-auth-pie a:hover { text-decoration: underline; }
@media (max-width: 420px) {
.modal-auth-caja { padding: 1.5rem 1.2rem 1.2rem; }
}

0
css/intenciones.css → frontend/css/intenciones.css Executable file → Normal file
View File

0
css/login.css → frontend/css/login.css Executable file → Normal file
View File

View File

0
css/register.css → frontend/css/register.css Executable file → Normal file
View File

View File

View File

0
data/difuntos.json → frontend/data/difuntos.json Executable file → Normal file
View File

View File

0
data/libros.json → frontend/data/libros.json Executable file → Normal file
View File

0
data/salmos.json → frontend/data/salmos.json Executable file → Normal file
View File

0
data/santos.json → frontend/data/santos.json Executable file → Normal file
View File

View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diario de Oración</title>
<link rel="icon" type="image/x-icon" href="img/favicon.png">
<link rel="stylesheet" href="css/estilos.css">
<link rel="stylesheet" href="css/diario.css">
</head>
<body>
<div id="header-container"></div>
<main class="contenedor-diario">
<!-- CABECERA -->
<div class="diario-cabecera">
<h2>🕯 Mi Diario de Oración</h2>
<p class="saludo-diario" id="saludo-usuario"></p>
</div>
<!-- EDITOR -->
<section class="bloque editor-diario" id="editor-diario">
<!-- Navegación de fecha -->
<div class="nav-fecha">
<button class="btn-nav" id="btn-anterior" title="Día anterior">&#8249;</button>
<div class="fecha-centro">
<span class="display-fecha" id="display-fecha"></span>
<input type="date" id="fecha-entrada" class="input-fecha-oculto" aria-label="Seleccionar fecha">
<button class="btn-cambiar-fecha" title="Elegir otra fecha"
onclick="document.getElementById('fecha-entrada').showPicker
? document.getElementById('fecha-entrada').showPicker()
: document.getElementById('fecha-entrada').focus()">📅</button>
</div>
<button class="btn-nav" id="btn-siguiente" title="Día siguiente">&#8250;</button>
</div>
<!-- Estado espiritual -->
<div class="estados-animo">
<p class="label-estados">¿Cómo estás hoy en tu interior?</p>
<div class="botones-estados">
<button class="btn-estado" data-estado="paz">🕊️ <span>Paz</span></button>
<button class="btn-estado" data-estado="gratitud">🙏 <span>Gratitud</span></button>
<button class="btn-estado" data-estado="lucha">😔 <span>Lucha</span></button>
<button class="btn-estado" data-estado="gozo"><span>Gozo</span></button>
<button class="btn-estado" data-estado="silencio">🌿 <span>Silencio</span></button>
</div>
</div>
<!-- Título opcional -->
<input type="text" id="titulo-entrada" placeholder="Título de la entrada (opcional)…"
class="input-titulo" maxlength="100">
<!-- Área de escritura -->
<textarea id="texto-entrada"
placeholder="Escribe tu oración, reflexión o lo que llevas en el corazón hoy…"
class="textarea-diario" rows="8"></textarea>
<!-- Botones -->
<div class="acciones-diario">
<button class="boton btn-guardar-diario" id="btn-guardar-entrada">Guardar ✝</button>
<button class="boton btn-borrar-diario" id="btn-borrar-entrada" style="display:none;">Eliminar</button>
</div>
<div id="mensaje-diario" class="mensaje-diario" style="display:none;"></div>
</section>
<!-- HISTORIAL DE ENTRADAS -->
<section class="bloque historial-diario">
<h3>📖 Mis entradas</h3>
<ul id="lista-entradas" class="lista-entradas"></ul>
<p id="sin-entradas" class="texto-suave" style="display:none;">
Aún no hay entradas en tu diario. ¡Empieza hoy mismo escribiendo lo que llevas en el corazón!
</p>
</section>
</main>
<script src="js/api-config.js"></script>
<script src="js/auth.js"></script>
<script src="js/header.js"></script>
<script src="js/diario.js"></script>
</body>
</html>

1
header.html → frontend/header.html Executable file → Normal file
View File

@ -30,6 +30,7 @@
<a href="rosario.html">Rosario</a>
<a href="oraciones-basicas.html">Oraciones Básicas</a>
<a href="intenciones.html">Intenciones</a>
<a href="diario-oracion.html">Diario</a>
<a href="biblioteca-cristiana.html">Biblioteca cristiana</a>
</nav>
<script src="js/header.js"></script>

0
img/biblia.jpg → frontend/img/biblia.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

0
img/biblioteca.jpg → frontend/img/biblioteca.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 217 KiB

0
img/dolorosos1.jpg → frontend/img/dolorosos1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.7 MiB

After

Width:  |  Height:  |  Size: 5.7 MiB

0
img/dolorosos2.jpg → frontend/img/dolorosos2.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 6.8 MiB

After

Width:  |  Height:  |  Size: 6.8 MiB

0
img/dolorosos3.jpg → frontend/img/dolorosos3.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.2 MiB

After

Width:  |  Height:  |  Size: 5.2 MiB

0
img/dolorosos4.jpg → frontend/img/dolorosos4.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.2 MiB

After

Width:  |  Height:  |  Size: 5.2 MiB

0
img/dolorosos5.jpg → frontend/img/dolorosos5.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.5 MiB

After

Width:  |  Height:  |  Size: 5.5 MiB

0
img/favicon.png → frontend/img/favicon.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

0
img/faviconClaro.png → frontend/img/faviconClaro.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

0
img/gloriosos1.jpg → frontend/img/gloriosos1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

0
img/gloriosos2.jpg → frontend/img/gloriosos2.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

0
img/gloriosos3.jpg → frontend/img/gloriosos3.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.4 MiB

After

Width:  |  Height:  |  Size: 5.4 MiB

0
img/gloriosos4.jpg → frontend/img/gloriosos4.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.5 MiB

After

Width:  |  Height:  |  Size: 5.5 MiB

0
img/gloriosos5.jpg → frontend/img/gloriosos5.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.3 MiB

After

Width:  |  Height:  |  Size: 5.3 MiB

0
img/gozosos1.jpg → frontend/img/gozosos1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 7.7 MiB

After

Width:  |  Height:  |  Size: 7.7 MiB

0
img/gozosos2.jpg → frontend/img/gozosos2.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 8.9 MiB

After

Width:  |  Height:  |  Size: 8.9 MiB

0
img/gozosos3.jpg → frontend/img/gozosos3.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 6.3 MiB

After

Width:  |  Height:  |  Size: 6.3 MiB

0
img/gozosos4.jpg → frontend/img/gozosos4.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.9 MiB

After

Width:  |  Height:  |  Size: 5.9 MiB

0
img/gozosos5.jpg → frontend/img/gozosos5.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 6.1 MiB

After

Width:  |  Height:  |  Size: 6.1 MiB

0
img/iconos/cruz.png → frontend/img/iconos/cruz.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

0
img/iconos/flor.png → frontend/img/iconos/flor.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

0
img/iconos/vela.png → frontend/img/iconos/vela.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Some files were not shown because too many files have changed in this diff Show More