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>
|
<section id="booksContainer" class="grid"></section>
|
||||||
|
|
||||||
|
<script src="js/api-config.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
<script src="js/header.js"></script>
|
<script src="js/header.js"></script>
|
||||||
<script src="js/biblioteca.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);
|
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 {
|
.btn-sesion {
|
||||||
padding: 0.3rem 0.8rem;
|
padding: 0.3rem 0.8rem;
|
||||||
background: rgba(255,255,255,0.15);
|
background: rgba(255,255,255,0.15);
|
||||||
color: white;
|
color: white;
|
||||||
border: 1px solid rgba(255,255,255,0.4);
|
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 {
|
.btn-sesion:hover {
|
||||||
background: rgba(255,255,255,0.3);
|
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="rosario.html">Rosario</a>
|
||||||
<a href="oraciones-basicas.html">Oraciones Básicas</a>
|
<a href="oraciones-basicas.html">Oraciones Básicas</a>
|
||||||
<a href="intenciones.html">Intenciones</a>
|
<a href="intenciones.html">Intenciones</a>
|
||||||
|
<a href="diario-oracion.html">Diario</a>
|
||||||
<a href="biblioteca-cristiana.html">Biblioteca cristiana</a>
|
<a href="biblioteca-cristiana.html">Biblioteca cristiana</a>
|
||||||
</nav>
|
</nav>
|
||||||
<script src="js/header.js"></script>
|
<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 |