frontend u backend
|
|
@ -0,0 +1,7 @@
|
|||
target/
|
||||
.git/
|
||||
.gitignore
|
||||
.env
|
||||
*.md
|
||||
src/test/
|
||||
docker/
|
||||
|
|
@ -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
|
||||
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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:
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package es.recursoscatolicos.model;
|
||||
|
||||
public enum Ambito {
|
||||
PERSONAL,
|
||||
PARROQUIA,
|
||||
GRUPO
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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> {
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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&crid=2X17ZIW20JHVF&keywords=biblia&qid=1686516688&sprefix=biblia%2Caps%2C94&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&keywords=catecismo+astete&qid=1686517828&s=books&sprefix=Catecismo+as%2Cstripbooks%2C80&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&qid=1686518245&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&crid=3JXXXQXP05Y9H&keywords=san+francisco+de+asis+luis+perez+simon&qid=1686518697&s=digital-text&sprefix=san+francisco+de+asis+luis+perez+simon%2Cdigital-text%2C77&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>
|
||||
|
|
@ -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,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; }
|
||||
}
|
||||
|
|
@ -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
data/calendario-liturgico-25-26.pdf → frontend/data/calendario-liturgico-25-26.pdf
Executable file → Normal file
0
data/calendario-liturgico.json → frontend/data/calendario-liturgico.json
Executable file → Normal file
0
data/colores-liturgicos.json → frontend/data/colores-liturgicos.json
Executable file → Normal 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">‹</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">›</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>
|
||||
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 5.7 MiB After Width: | Height: | Size: 5.7 MiB |
|
Before Width: | Height: | Size: 6.8 MiB After Width: | Height: | Size: 6.8 MiB |
|
Before Width: | Height: | Size: 5.2 MiB After Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 5.2 MiB After Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 5.5 MiB After Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 5.0 MiB After Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 5.4 MiB After Width: | Height: | Size: 5.4 MiB |
|
Before Width: | Height: | Size: 5.5 MiB After Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 5.3 MiB After Width: | Height: | Size: 5.3 MiB |
|
Before Width: | Height: | Size: 7.7 MiB After Width: | Height: | Size: 7.7 MiB |
|
Before Width: | Height: | Size: 8.9 MiB After Width: | Height: | Size: 8.9 MiB |
|
Before Width: | Height: | Size: 6.3 MiB After Width: | Height: | Size: 6.3 MiB |
|
Before Width: | Height: | Size: 5.9 MiB After Width: | Height: | Size: 5.9 MiB |
|
Before Width: | Height: | Size: 6.1 MiB After Width: | Height: | Size: 6.1 MiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.6 MiB |
0
img/libros/catecismo-astete.jpg → frontend/img/libros/catecismo-astete.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
0
img/libros/catecismo-astete.png → frontend/img/libros/catecismo-astete.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |