diff --git a/backend/src/main/java/dev/flima/infrastructure/config/ApplicationDefault.java b/backend/src/main/java/dev/flima/infrastructure/config/ApplicationDefault.java new file mode 100644 index 0000000..473e93c --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/config/ApplicationDefault.java @@ -0,0 +1,8 @@ +package dev.flima.infrastructure.config; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("api/v1") +public class ApplicationDefault extends Application { +} diff --git a/backend/src/main/java/dev/flima/infrastructure/contents/ContentPanacheEntity.java b/backend/src/main/java/dev/flima/infrastructure/contents/ContentPanacheEntity.java new file mode 100644 index 0000000..03de127 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/contents/ContentPanacheEntity.java @@ -0,0 +1,27 @@ +package dev.flima.infrastructure.contents; + +import dev.flima.domain.contents.SectionType; +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.UUID; + +@Entity(name = "contents") +public class ContentPanacheEntity extends PanacheEntityBase { + @Id + public UUID id; + + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.NAMED_ENUM) + @Column(name = "section_type", nullable = false) + @NotNull + public SectionType sectionType; + + @Embedded + @Column(nullable = false) + public SectionContentEmbeddable content; +} \ No newline at end of file diff --git a/backend/src/main/java/dev/flima/infrastructure/contents/ContentRepositoryImpl.java b/backend/src/main/java/dev/flima/infrastructure/contents/ContentRepositoryImpl.java new file mode 100644 index 0000000..08417e6 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/contents/ContentRepositoryImpl.java @@ -0,0 +1,90 @@ +package dev.flima.infrastructure.contents; + +import dev.flima.domain.contents.Content; +import dev.flima.domain.contents.ContentRepository; +import dev.flima.domain.contents.SectionContent; +import dev.flima.domain.exceptions.EntityNotFoundException; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.NotFoundException; + +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.UUID; + +@ApplicationScoped +public class ContentRepositoryImpl implements ContentRepository, PanacheRepositoryBase { + + private final ResourceBundle messages = ResourceBundle.getBundle("messages"); + + @Override + public void save(Content content) { + ContentPanacheEntity entity = new ContentPanacheEntity(); + + entity.id = content.getId(); + entity.sectionType = content.getSectionType(); + + if(content.getSectionContent() != null) { + entity.content = new SectionContentEmbeddable(); + entity.content.title = content.getSectionContent().title(); + entity.content.subtitle = content.getSectionContent().subtitle(); + } + + persist(entity); + } + + @Override + public void modify(Content content) { + ContentPanacheEntity entity = findById(content.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("content.not_found")); + } + + entity.sectionType = content.getSectionType(); + + if(content.getSectionContent() != null) { + entity.content = new SectionContentEmbeddable(); + entity.content.title = content.getSectionContent().title(); + entity.content.subtitle = content.getSectionContent().subtitle(); + } + } + + @Override + public Optional getById(UUID id) { + ContentPanacheEntity entity = findById(id); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("content.not_found")); + } + + String contentTitle = entity.content.title; + String contentSubtitle = entity.content.subtitle; + SectionContent content = new SectionContent(contentTitle, contentSubtitle); + + return Optional.of(new Content(entity.id, entity.sectionType, content)); + } + + @Override + public List getAll() { + return findAll().list().stream() + .map(entity -> new Content( + entity.id, + entity.sectionType, + new SectionContent(entity.content.title, entity.content.subtitle) + )) + .toList(); + } + + @Override + public void remove(Content content) { + ContentPanacheEntity entity = findById(content.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("content.not_found")); + } + + delete(entity); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/contents/SectionContentEmbeddable.java b/backend/src/main/java/dev/flima/infrastructure/contents/SectionContentEmbeddable.java new file mode 100644 index 0000000..d8607be --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/contents/SectionContentEmbeddable.java @@ -0,0 +1,16 @@ +package dev.flima.infrastructure.contents; + +import jakarta.persistence.Embeddable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Embeddable +public class SectionContentEmbeddable { + @NotNull(message = "Title cannot be null.") + @NotBlank(message = "Title cannot be blank.") + public String title; + + @NotNull(message = "Subtitle cannot be null.") + @NotBlank(message = "Subtitle cannot be blank.") + public String subtitle; +} diff --git a/backend/src/main/java/dev/flima/infrastructure/educations/EducationPanacheEntity.java b/backend/src/main/java/dev/flima/infrastructure/educations/EducationPanacheEntity.java new file mode 100644 index 0000000..cf800bd --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/educations/EducationPanacheEntity.java @@ -0,0 +1,58 @@ +package dev.flima.infrastructure.educations; + +import dev.flima.domain.educations.TypeEducation; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.List; +import java.util.UUID; + +@Entity(name = "educations") +public class EducationPanacheEntity { + + @Id + public UUID id; + + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.NAMED_ENUM) + @Column(name = "education_type", nullable = false) + @NotNull + public TypeEducation typeEducation; + + @Column(nullable = false) + @NotBlank + public String degree; + + @Column(nullable = false) + @NotBlank + public String title; + + @Column(nullable = false) + @NotBlank + public String institution; + + @Column(nullable = false) + @NotBlank + public String period; + + @Column(nullable = false) + @NotBlank + public String specialization; + + @ElementCollection + @CollectionTable(name = "education_skills", joinColumns = @JoinColumn(name = "education_id")) + @Column(name = "skill") + @NotEmpty + public List skills; + + @ElementCollection + @CollectionTable(name = "education_architectures", joinColumns = @JoinColumn(name = "education_id")) + @Column(name = "architecture") + @NotEmpty + public List architectures; + +} diff --git a/backend/src/main/java/dev/flima/infrastructure/educations/EducationRepositoryImpl.java b/backend/src/main/java/dev/flima/infrastructure/educations/EducationRepositoryImpl.java new file mode 100644 index 0000000..0f53ed2 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/educations/EducationRepositoryImpl.java @@ -0,0 +1,103 @@ +package dev.flima.infrastructure.educations; + +import dev.flima.domain.educations.Education; +import dev.flima.domain.educations.EducationRepository; +import dev.flima.domain.exceptions.EntityNotFoundException; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.NotFoundException; + +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.UUID; + +@ApplicationScoped +public class EducationRepositoryImpl implements EducationRepository, PanacheRepositoryBase { + + private final ResourceBundle messages = ResourceBundle.getBundle("messages"); + + @Override + public void save(Education education) { + EducationPanacheEntity entity = new EducationPanacheEntity(); + + entity.id = education.getId(); + entity.typeEducation = education.getTypeEducation(); + entity.degree = education.getDegree(); + entity.title = education.getTitle(); + entity.institution = education.getInstitution(); + entity.period = education.getPeriod(); + entity.specialization = education.getSpecialization(); + entity.skills = education.getSkills(); + entity.architectures = education.getArchitectures(); + + persist(entity); + } + + @Override + public void modify(Education education) { + EducationPanacheEntity entity = findById(education.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("education.not_found")); + } + + entity.typeEducation = education.getTypeEducation(); + entity.degree = education.getDegree(); + entity.title = education.getTitle(); + entity.institution = education.getInstitution(); + entity.period = education.getPeriod(); + entity.specialization = education.getSpecialization(); + entity.skills = education.getSkills(); + entity.architectures = education.getArchitectures(); + } + + @Override + public Optional getById(UUID id) { + EducationPanacheEntity entity = findById(id); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("education.not_found")); + } + + return Optional.of(new Education( + entity.id, + entity.typeEducation, + entity.degree, + entity.title, + entity.institution, + entity.period, + entity.specialization, + entity.skills, + entity.architectures + )); + } + + @Override + public List getAll() { + return findAll().list().stream() + .map(entity -> new Education( + entity.id, + entity.typeEducation, + entity.degree, + entity.title, + entity.institution, + entity.period, + entity.specialization, + entity.skills, + entity.architectures + )) + .toList(); + } + + @Override + public void remove(Education education) { + EducationPanacheEntity entity = findById(education.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("education.not_found")); + } + + delete(entity); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/exceptions/ConstraintViolationExceptionHandler.java b/backend/src/main/java/dev/flima/infrastructure/exceptions/ConstraintViolationExceptionHandler.java new file mode 100644 index 0000000..7b3bdeb --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/exceptions/ConstraintViolationExceptionHandler.java @@ -0,0 +1,23 @@ +package dev.flima.infrastructure.exceptions; + +import dev.flima.presentation.rest.dto.ErrorResponse; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import java.util.List; + +@Provider +public class ConstraintViolationExceptionHandler implements ExceptionMapper { + + @Override + public Response toResponse(ConstraintViolationException ex) { + List details = ex.getConstraintViolations().stream() + .map(v -> v.getPropertyPath() + ": " + v.getMessage()) + .toList(); + + return Response.status(400) + .entity(ErrorResponse.of("Validation failed", 400, details)) + .build(); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/exceptions/DatabaseExceptionHandler.java b/backend/src/main/java/dev/flima/infrastructure/exceptions/DatabaseExceptionHandler.java new file mode 100644 index 0000000..6a73182 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/exceptions/DatabaseExceptionHandler.java @@ -0,0 +1,29 @@ +package dev.flima.infrastructure.exceptions; + +import dev.flima.presentation.rest.dto.ErrorResponse; +import jakarta.persistence.PersistenceException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import org.hibernate.exception.ConstraintViolationException; + +@Provider +public class DatabaseExceptionHandler implements ExceptionMapper { + + @Override + public Response toResponse(PersistenceException ex) { + Throwable cause = ex; + while (cause != null) { + if (cause instanceof ConstraintViolationException) { + return Response.status(409) + .entity(ErrorResponse.of("Record already exists with this unique identifier.", 409)) + .build(); + } + cause = cause.getCause(); + } + + return Response.status(500) + .entity(ErrorResponse.of("Database error occurred.", 500)) + .build(); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/exceptions/DomainExceptionHandler.java b/backend/src/main/java/dev/flima/infrastructure/exceptions/DomainExceptionHandler.java new file mode 100644 index 0000000..2afe7d4 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/exceptions/DomainExceptionHandler.java @@ -0,0 +1,18 @@ +package dev.flima.infrastructure.exceptions; + +import dev.flima.domain.exceptions.DomainException; +import dev.flima.presentation.rest.dto.ErrorResponse; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class DomainExceptionHandler implements ExceptionMapper { + + @Override + public Response toResponse(DomainException ex) { + return Response.status(ex.getStatusCode()) + .entity(ErrorResponse.of(ex.getMessage(), ex.getStatusCode())) + .build(); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/experience/ExperiencePanacheEntity.java b/backend/src/main/java/dev/flima/infrastructure/experience/ExperiencePanacheEntity.java new file mode 100644 index 0000000..5f33c94 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/experience/ExperiencePanacheEntity.java @@ -0,0 +1,45 @@ +package dev.flima.infrastructure.experience; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.List; +import java.util.UUID; + +@Entity(name = "experiences") +public class ExperiencePanacheEntity { + + @Id + public UUID id; + + @Column(nullable = false) + @NotBlank + public String title; + + @Column(nullable = false) + @NotBlank + public String company; + + @Column(nullable = false) + @NotBlank + public String period; + + @ElementCollection + @CollectionTable(name = "experience_bullets", joinColumns = @JoinColumn(name = "experience_id")) + @Column(name = "bullet") + @NotEmpty + public List bullets; + + @ElementCollection + @CollectionTable(name = "experience_technologies", joinColumns = @JoinColumn(name = "experience_id")) + @Column(name = "technology") + @NotEmpty + public List technologies; + + @Column(nullable = false) + @NotBlank + public String icon; + +} diff --git a/backend/src/main/java/dev/flima/infrastructure/experience/ExperienceRepositoryImpl.java b/backend/src/main/java/dev/flima/infrastructure/experience/ExperienceRepositoryImpl.java new file mode 100644 index 0000000..a89f9c1 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/experience/ExperienceRepositoryImpl.java @@ -0,0 +1,95 @@ +package dev.flima.infrastructure.experience; + +import dev.flima.domain.exceptions.EntityNotFoundException; +import dev.flima.domain.experience.Experience; +import dev.flima.domain.experience.ExperienceRepository; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.NotFoundException; + +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.UUID; + +@ApplicationScoped +public class ExperienceRepositoryImpl implements ExperienceRepository, PanacheRepositoryBase { + + private final ResourceBundle messages = ResourceBundle.getBundle("messages"); + + @Override + public void save(Experience experience) { + ExperiencePanacheEntity entity = new ExperiencePanacheEntity(); + + entity.id = experience.getId(); + entity.title = experience.getTitle(); + entity.company = experience.getCompany(); + entity.period = experience.getPeriod(); + entity.bullets = experience.getBullets(); + entity.technologies = experience.getTechnologies(); + entity.icon = experience.getIcon(); + + persist(entity); + } + + @Override + public void modify(Experience experience) { + ExperiencePanacheEntity entity = findById(experience.getId()); + + if(entity == null) { + throw new EntityNotFoundException(messages.getString("experience.not_found")); + } + + entity.title = experience.getTitle(); + entity.company = experience.getCompany(); + entity.period = experience.getPeriod(); + entity.bullets = experience.getBullets(); + entity.technologies = experience.getTechnologies(); + entity.icon = experience.getIcon(); + } + + @Override + public Optional getById(UUID id) { + ExperiencePanacheEntity entity = findById(id); + + if(entity == null) { + throw new EntityNotFoundException(messages.getString("experience.not_found")); + } + + return Optional.of(new Experience( + entity.id, + entity.title, + entity.company, + entity.period, + entity.bullets, + entity.technologies, + entity.icon + )); + } + + @Override + public List getAll() { + return findAll().list().stream() + .map(entity -> new Experience( + entity.id, + entity.title, + entity.company, + entity.period, + entity.bullets, + entity.technologies, + entity.icon + )) + .toList(); + } + + @Override + public void remove(Experience experience) { + ExperiencePanacheEntity entity = findById(experience.getId()); + + if(entity == null) { + throw new EntityNotFoundException(messages.getString("experience.not_found")); + } + + delete(entity); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/messages/MessagePanacheEntity.java b/backend/src/main/java/dev/flima/infrastructure/messages/MessagePanacheEntity.java new file mode 100644 index 0000000..82da97a --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/messages/MessagePanacheEntity.java @@ -0,0 +1,45 @@ +package dev.flima.infrastructure.messages; + +import dev.flima.domain.messages.StatusMessage; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity(name = "messages") +public class MessagePanacheEntity { + + @Id + public UUID id; + + @Column(nullable = false) + @NotBlank + public String username; + + @Column(nullable = false) + @NotBlank + public String email; + + @Column(nullable = false) + @NotBlank + public String subject; + + @Column(nullable = false) + @NotBlank + public String message; + + @Column(nullable = false) + @NotNull + public LocalDateTime timestamp; + + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.NAMED_ENUM) + @Column(name = "status_message", nullable = false) + @NotNull + public StatusMessage statusMessage; + +} diff --git a/backend/src/main/java/dev/flima/infrastructure/messages/MessageRepositoryImpl.java b/backend/src/main/java/dev/flima/infrastructure/messages/MessageRepositoryImpl.java new file mode 100644 index 0000000..0a7eecd --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/messages/MessageRepositoryImpl.java @@ -0,0 +1,93 @@ +package dev.flima.infrastructure.messages; + +import dev.flima.domain.exceptions.EntityNotFoundException; +import dev.flima.domain.messages.Message; +import dev.flima.domain.messages.MessageRepository; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.NotFoundException; + +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.UUID; + +@ApplicationScoped +public class MessageRepositoryImpl implements MessageRepository, PanacheRepositoryBase { + + private final ResourceBundle messages = ResourceBundle.getBundle("messages"); + @Override + public void save(Message message) { + MessagePanacheEntity entity = new MessagePanacheEntity(); + + entity.id = message.getId(); + entity.username = message.getUsername(); + entity.email = message.getEmail(); + entity.subject = message.getSubject(); + entity.message = message.getMessage(); + entity.timestamp = message.getTimestamp(); + entity.statusMessage = message.getStatusMessage(); + + persist(entity); + } + + @Override + public void modify(Message message) { + MessagePanacheEntity entity = findById(message.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("message.not_found")); + } + + entity.username = message.getUsername(); + entity.email = message.getEmail(); + entity.subject = message.getSubject(); + entity.message = message.getMessage(); + entity.statusMessage = message.getStatusMessage(); + } + + @Override + public Optional getById(UUID id) { + MessagePanacheEntity entity = findById(id); + + if (entity == null) { + throw new NotFoundException("Message not found"); + } + + return Optional.of(new Message( + entity.id, + entity.username, + entity.email, + entity.subject, + entity.message, + entity.timestamp, + entity.statusMessage + )); + } + + @Override + public List getAll() { + return findAll().list().stream() + .map(entity -> new Message( + entity.id, + entity.username, + entity.email, + entity.subject, + entity.message, + entity.timestamp, + entity.statusMessage + )) + .toList(); + } + + @Override + public void remove(Message message) { + MessagePanacheEntity entity = findById(message.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("message.not_found")); + } + + delete(entity); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/messaging/ContactMessagingAdapter.java b/backend/src/main/java/dev/flima/infrastructure/messaging/ContactMessagingAdapter.java new file mode 100644 index 0000000..ee31feb --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/messaging/ContactMessagingAdapter.java @@ -0,0 +1,64 @@ +package dev.flima.infrastructure.messaging; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.flima.application.messages.dtos.request.MessageDTORequest; +import dev.flima.application.messages.usecases.PersistMessageUseCase; +import dev.flima.domain.messages.Message; +import dev.flima.domain.messages.MessageProducerPort; +import dev.flima.infrastructure.messages.MessagePanacheEntity; +import dev.flima.infrastructure.messages.MessageRepositoryImpl; +import io.quarkus.logging.Log; +import io.smallrye.common.annotation.Blocking; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Incoming; + +@ApplicationScoped +public class ContactMessagingAdapter implements MessageProducerPort { + + @Inject + ObjectMapper objectMapper; + + @Inject + PersistMessageUseCase persistMessageUseCase; + + @Channel("contact-out") + Emitter contactEmitter; + + @Override + public void sendMessage(Message message) { + try { + String json = objectMapper.writeValueAsString(message); + contactEmitter.send(json); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error serializing message", e); + } + } + + @Incoming("contact-in") + @Blocking + @Transactional + public void processContactResponse(String messagePayload) { + try { + Message messageReceive = objectMapper.readValue(messagePayload, Message.class); + + Message message = new Message( + messageReceive.getUsername(), + messageReceive.getEmail(), + messageReceive.getSubject(), + messageReceive.getMessage() + ); + + persistMessageUseCase.execute(message); + } catch (JsonProcessingException e) { + Log.error("Falha ao processar mensagem. Enviando para DLQ: " + messagePayload, e); + throw new RuntimeException(e); + } + } + +} diff --git a/backend/src/main/java/dev/flima/infrastructure/monitoring/DatabaseHealthCheck.java b/backend/src/main/java/dev/flima/infrastructure/monitoring/DatabaseHealthCheck.java new file mode 100644 index 0000000..b02d9f7 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/monitoring/DatabaseHealthCheck.java @@ -0,0 +1,32 @@ +package dev.flima.infrastructure.monitoring; + +import dev.flima.domain.users.UserRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Readiness; + +@Readiness +@ApplicationScoped +public class DatabaseHealthCheck implements HealthCheck { + + @Inject + UserRepository userRepository; + + @Override + public HealthCheckResponse call() { + try { + userRepository.count(); + return HealthCheckResponse.named("PostgreSQL Connection") + .up() + .withData("database", "PostgreSQL") + .build(); + } catch (Exception e) { + return HealthCheckResponse.named("PostgreSQL Connection") + .down() + .withData("error", e.getMessage()) + .build(); + } + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/projects/ProjectPanacheEntity.java b/backend/src/main/java/dev/flima/infrastructure/projects/ProjectPanacheEntity.java new file mode 100644 index 0000000..6b84e34 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/projects/ProjectPanacheEntity.java @@ -0,0 +1,38 @@ +package dev.flima.infrastructure.projects; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.List; +import java.util.UUID; + +@Entity(name = "projects") +public class ProjectPanacheEntity { + + @Id + public UUID id; + + @Column(nullable = false) + @NotBlank + public String title; + + @Column(nullable = false) + @NotBlank + public String subtitle; + + @Column(nullable = false) + @NotBlank + public String description; + + @ElementCollection + @CollectionTable(name = "project_technologies", joinColumns = @JoinColumn(name = "project_id")) + @Column(name = "technology") + @NotEmpty + public List technologies; + + public String codeSnippet; + public String icon; + +} diff --git a/backend/src/main/java/dev/flima/infrastructure/projects/ProjectRepositoryImpl.java b/backend/src/main/java/dev/flima/infrastructure/projects/ProjectRepositoryImpl.java new file mode 100644 index 0000000..4138eef --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/projects/ProjectRepositoryImpl.java @@ -0,0 +1,94 @@ +package dev.flima.infrastructure.projects; + +import dev.flima.domain.exceptions.EntityNotFoundException; +import dev.flima.domain.projects.Project; +import dev.flima.domain.projects.ProjectRepository; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.UUID; + +@ApplicationScoped +public class ProjectRepositoryImpl implements ProjectRepository, PanacheRepositoryBase { + + private ResourceBundle messages = ResourceBundle.getBundle("messages"); + + @Override + public void save(Project project) { + ProjectPanacheEntity entity = new ProjectPanacheEntity(); + + entity.id = project.getId(); + entity.title = project.getTitle(); + entity.subtitle = project.getSubtitle(); + entity.description = project.getDescription(); + entity.technologies = project.getTechnologies(); + entity.codeSnippet = project.getCodeSnippet(); + entity.icon = project.getIcon(); + + persist(entity); + } + + @Override + public void modify(Project project) { + ProjectPanacheEntity entity = findById(project.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("project.not_found")); + } + + entity.title = project.getTitle(); + entity.subtitle = project.getSubtitle(); + entity.description = project.getDescription(); + entity.technologies = project.getTechnologies(); + entity.codeSnippet = project.getCodeSnippet(); + entity.icon = project.getIcon(); + } + + @Override + public Optional getById(UUID id) { + ProjectPanacheEntity entity = findById(id); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("project.not_found")); + } + + return Optional.of(new Project( + entity.id, + entity.title, + entity.subtitle, + entity.description, + entity.technologies, + entity.codeSnippet, + entity.icon + )); + } + + @Override + public List getAll() { + return findAll().list().stream() + .map(project -> new Project( + project.id, + project.title, + project.subtitle, + project.description, + project.technologies, + project.codeSnippet, + project.icon + )) + .toList(); + } + + @Override + public void remove(Project project) { + ProjectPanacheEntity entity = findById(project.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("project.not_found")); + } + + delete(entity); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/security/JwtTokenGeneratorImpl.java b/backend/src/main/java/dev/flima/infrastructure/security/JwtTokenGeneratorImpl.java new file mode 100644 index 0000000..375141f --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/security/JwtTokenGeneratorImpl.java @@ -0,0 +1,20 @@ +package dev.flima.infrastructure.security; + +import dev.flima.domain.security.TokenGenerator; +import io.smallrye.jwt.build.Jwt; +import jakarta.enterprise.context.ApplicationScoped; + +import java.time.Duration; +import java.util.Set; + +@ApplicationScoped +public class JwtTokenGeneratorImpl implements TokenGenerator { + @Override + public String generateToken(String username, String role) { + return Jwt.issuer("https://flima.dev") + .upn(username) + .groups(Set.of(role)) + .expiresIn(Duration.ofMinutes(60)) + .sign(); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/security/PasswordHasherImpl.java b/backend/src/main/java/dev/flima/infrastructure/security/PasswordHasherImpl.java new file mode 100644 index 0000000..1ce0deb --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/security/PasswordHasherImpl.java @@ -0,0 +1,33 @@ +package dev.flima.infrastructure.security; + +import com.password4j.Argon2Function; +import com.password4j.types.Argon2; +import dev.flima.domain.security.PasswordHasher; +import dev.flima.domain.users.Password; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PasswordHasherImpl implements PasswordHasher { + + private final Argon2Function ARGON2 = Argon2Function.getInstance( + 65536, + 16, + 1, + 32, + Argon2.ID + ); + + @Override + public boolean verify(Password plainPassword, Password hashedPassword) { + try { + return ARGON2.check(plainPassword.password(), hashedPassword.password()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + @Override + public String hash(Password plainPassword) { + return ARGON2.hash(plainPassword.password()).getResult(); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/stacks/StackPanacheEntity.java b/backend/src/main/java/dev/flima/infrastructure/stacks/StackPanacheEntity.java new file mode 100644 index 0000000..ce782fe --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/stacks/StackPanacheEntity.java @@ -0,0 +1,32 @@ +package dev.flima.infrastructure.stacks; + +import dev.flima.domain.stacks.StackType; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.List; +import java.util.UUID; + +@Entity(name = "stacks") +public class StackPanacheEntity { + + @Id + public UUID id; + + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.NAMED_ENUM) + @Column(name = "stack_type", nullable = false) + @NotNull + public StackType stackType; + + @ElementCollection + @CollectionTable(name = "stack_technologies", joinColumns = @JoinColumn(name = "stack_id")) + @Column(name = "technology") + @NotEmpty + public List technologies; + +} diff --git a/backend/src/main/java/dev/flima/infrastructure/stacks/StackRepositoryImpl.java b/backend/src/main/java/dev/flima/infrastructure/stacks/StackRepositoryImpl.java new file mode 100644 index 0000000..8920dfc --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/stacks/StackRepositoryImpl.java @@ -0,0 +1,75 @@ +package dev.flima.infrastructure.stacks; + +import dev.flima.domain.exceptions.EntityNotFoundException; +import dev.flima.domain.stacks.Stack; +import dev.flima.domain.stacks.StackRepository; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.NotFoundException; + +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.UUID; + +@ApplicationScoped +public class StackRepositoryImpl implements StackRepository, PanacheRepositoryBase { + + private final ResourceBundle messages = ResourceBundle.getBundle("messages"); + + @Override + public void save(Stack stack) { + StackPanacheEntity entity = new StackPanacheEntity(); + + entity.id = stack.getId(); + entity.stackType = stack.getStackType(); + entity.technologies = stack.getTechnologies(); + + persist(entity); + } + + @Override + public void modify(Stack stack) { + StackPanacheEntity entity = findById(stack.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("stack.not_found")); + } + + entity.stackType = stack.getStackType(); + entity.technologies = stack.getTechnologies(); + } + + @Override + public Optional getById(UUID id) { + StackPanacheEntity entity = findById(id); + + if(entity == null) { + throw new EntityNotFoundException(messages.getString("stack.not_found")); + } + + return Optional.of(new Stack(entity.id, entity.stackType, entity.technologies)); + } + + @Override + public List getAll() { + return findAll().list().stream() + .map(entity -> new Stack( + entity.id, + entity.stackType, + entity.technologies + )) + .toList(); + } + + @Override + public void remove(Stack stack) { + StackPanacheEntity entity = findById(stack.getId()); + + if (entity == null) { + throw new EntityNotFoundException(messages.getString("stack.not_found")); + } + + delete(entity); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/stats/StatPanacheEntity.java b/backend/src/main/java/dev/flima/infrastructure/stats/StatPanacheEntity.java new file mode 100644 index 0000000..a2a99b0 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/stats/StatPanacheEntity.java @@ -0,0 +1,41 @@ +package dev.flima.infrastructure.stats; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +@Entity(name = "stats") +public class StatPanacheEntity { + + @Id + public UUID id; + + @Column(nullable = false) + @NotBlank + public String yearsExperience; + + @Column(nullable = false) + @NotBlank + public String systemDeployed; + + @Column(nullable = false) + @NotBlank + public String uptimeSLA; + + @Column(nullable = false) + @NotBlank + public String commitsLogged; + + @Column(nullable = false) + @NotBlank + public String status; + + @Column(nullable = false) + @NotBlank + public String objective; + +} diff --git a/backend/src/main/java/dev/flima/infrastructure/stats/StatRespositoryImpl.java b/backend/src/main/java/dev/flima/infrastructure/stats/StatRespositoryImpl.java new file mode 100644 index 0000000..2930149 --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/stats/StatRespositoryImpl.java @@ -0,0 +1,93 @@ +package dev.flima.infrastructure.stats; + +import dev.flima.domain.exceptions.EntityNotFoundException; +import dev.flima.domain.stats.Stat; +import dev.flima.domain.stats.StatRepository; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.UUID; + +@ApplicationScoped +public class StatRespositoryImpl implements StatRepository, PanacheRepositoryBase { + + private final ResourceBundle messages = ResourceBundle.getBundle("messages"); + + @Override + public void save(Stat stat) { + StatPanacheEntity entity = new StatPanacheEntity(); + + entity.id = stat.getId(); + entity.yearsExperience = stat.getYearsExperience(); + entity.systemDeployed = stat.getSystemDeployed(); + entity.uptimeSLA = stat.getUptimeSLA(); + entity.status = stat.getStatus(); + entity.objective = stat.getObjective(); + + persist(entity); + } + + @Override + public void modify(Stat stat) { + StatPanacheEntity entity = findById(stat.getId()); + + if(entity == null) { + throw new EntityNotFoundException(messages.getString("stat.not_found")); + } + + entity.yearsExperience = stat.getYearsExperience(); + entity.systemDeployed = stat.getSystemDeployed(); + entity.uptimeSLA = stat.getUptimeSLA(); + entity.commitsLogged = stat.getCommitsLogged(); + entity.status = stat.getStatus(); + entity.objective = stat.getObjective(); + } + + @Override + public Optional getById(UUID id) { + StatPanacheEntity entity = findById(id); + + if(entity == null) { + throw new EntityNotFoundException(messages.getString("stat.not_found")); + } + + return Optional.of(new Stat( + entity.id, + entity.yearsExperience, + entity.systemDeployed, + entity.uptimeSLA, + entity.commitsLogged, + entity.status, + entity.objective + )); + } + + @Override + public List getAll() { + return findAll().list().stream() + .map(entity -> new Stat( + entity.id, + entity.yearsExperience, + entity.systemDeployed, + entity.uptimeSLA, + entity.commitsLogged, + entity.status, + entity.objective + )) + .toList(); + } + + @Override + public void remove(Stat stat) { + StatPanacheEntity entity = findById(stat.getId()); + + if(entity == null) { + throw new EntityNotFoundException(messages.getString("stat.not_found")); + } + + delete(entity); + } +} diff --git a/backend/src/main/java/dev/flima/infrastructure/users/UserPanacheEntity.java b/backend/src/main/java/dev/flima/infrastructure/users/UserPanacheEntity.java new file mode 100644 index 0000000..fcfaf7b --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/users/UserPanacheEntity.java @@ -0,0 +1,44 @@ +package dev.flima.infrastructure.users; + +import dev.flima.domain.users.Role; +import io.smallrye.common.constraint.NotNull; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.UUID; + +@Entity(name = "users") +public class UserPanacheEntity { + + @Id + public UUID id; + + @Column(nullable = false) + @NotBlank + public String username; + + @Column(nullable = false) + @NotBlank + public String name; + + @Column(name = "last_name", nullable = false) + @NotBlank + public String lastName; + + @Column(nullable = false) + @NotBlank + public String email; + + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.NAMED_ENUM) + @Column(nullable = false) + @NotNull + public Role role; + + @Column(nullable = false) + @NotBlank + public String password; + +} diff --git a/backend/src/main/java/dev/flima/infrastructure/users/UserRepositoryImpl.java b/backend/src/main/java/dev/flima/infrastructure/users/UserRepositoryImpl.java new file mode 100644 index 0000000..6573c9e --- /dev/null +++ b/backend/src/main/java/dev/flima/infrastructure/users/UserRepositoryImpl.java @@ -0,0 +1,56 @@ +package dev.flima.infrastructure.users; + +import dev.flima.domain.users.Password; +import dev.flima.domain.users.User; +import dev.flima.domain.users.UserRepository; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.UUID; + +@ApplicationScoped +public class UserRepositoryImpl implements UserRepository, PanacheRepositoryBase { + + private final ResourceBundle messages = ResourceBundle.getBundle("messages"); + + @Override + public long count() { + return PanacheRepositoryBase.super.count(); + } + + @Override + public void save(User user) { + UserPanacheEntity entity = new UserPanacheEntity(); + + entity.id = user.getId(); + entity.username = user.getUsername(); + entity.name = user.getName(); + entity.lastName = user.getLastName(); + entity.email = user.getEmail(); + entity.role = user.getRole(); + entity.password = user.getPassword().password(); + + persist(entity); + } + + @Override + public Optional getUsername(String username) { + UserPanacheEntity entity = find("username", username).firstResult(); + + if(entity == null) { + return Optional.empty(); + } + + return Optional.of(new User( + entity.id, + entity.username, + entity.name, + entity.lastName, + entity.email, + entity.role, + new Password(entity.password) + )); + } +} diff --git a/backend/src/main/resources/ValidationMessages.properties b/backend/src/main/resources/ValidationMessages.properties new file mode 100644 index 0000000..2cf1a50 --- /dev/null +++ b/backend/src/main/resources/ValidationMessages.properties @@ -0,0 +1,80 @@ +# --- Mensagens Globais de Validação (Bean Validation) --- +global.id.required=Identifier is required for this operation. +global.field.invalid=The provided value for the field is invalid. +title.not_null_or_empty=Title is required. +subtitle.not_null_or_empty=Subtitle is required. +period.not_null_or_empty=Period is required. +icon.not_null_or_empty=Icon is required. +technologies.not_null_or_empty=Technology is required. +username.not_null=Username is required. +email.not_null=Email is required. +email.invalid=Please provide a valid email address. + +# --- Mensagens para Contents --- +content.not_found=Content record not found with the provided ID. +content.section_type.not_null=Section type is required (e.g., HOME, PROJECTS, EXPERIENCE, EDUCATION, CONTACT). +content.section_content.not_null=The actual content body for the section cannot be empty. + +# --- Mensagens para Educations --- +education.not_found=Education record not found. +education.type_education.not_null=Degree or education type is required. +education.institution.not_null=Educational institution name is required. +education.specialization.not_null=Field of study or specialization is required. +education.skills.not_null=At least one skill must be linked to this education. +education.architectures.not_null=Architectural concepts studied are required. + +# --- Mensagens para Experiences --- +experience.not_found=Experience record not found. +experience.company.not_null=Company name is required. +experience.bullets.not_null=A list of responsibilities (bullets) is required. + +# --- Mensagens para Messages (Contact Form) --- +message.not_found=Message not found. +message.subject.not_null=Subject is required to send a message. +message.message.not_null=Message body cannot be empty. +message.timestamp.not_null=Message timestamp is missing. +message.status_message.not_null=Message status must be defined (e.g., REPLIED, UNREAD, READ). +message.already_replied=This message has already been replied to and cannot be modified. +message.reply_empty=The reply content cannot be empty. +message.invalid_status_for_reply=The message status is invalid for a reply operation. +message.trying.reply="There was a problem trying to reply to the message." + +# --- Mensagens para Projects --- +project.not_found=Project not found. +project.description.not_null=Project description is required. +project.codeSnippet.not_null=A representative code snippet is required for the portfolio. + +# --- Mensagens para Stacks --- +stack.not_found=Stack category not found. +stack.stack_type.not_null=Stack type is required (e.g., LANGUAGES, DATABASES, INFRASTRUCTURE, MESSAGING). + +# --- Mensagens para Stats --- +stat.not_found=Stat record not found. +stat.years_experience.not_null=Years of experience count is required. +stat.system_deployed.not_null=Number of systems deployed is required. +stat.uptime_sla.not_null=Uptime SLA percentage is required. +stat.commits_logged.not_null=Total commits count is required. +stat.status.not_null=System current status is required. +stat.objective.not_null=Professional objective statement is required. + +# --- Mensagens para Users --- +user.not_found=User not found. +user.name.not_null=First name is required. +user.last_name.not_null=Last name is required. +user.role.not_null=User role must be assigned. +user.password.not_null=Password is required. +user.password.weak=Password does not meet the minimum security requirements. + +# --- EXCEÇÕES DE NEGÓCIO E SEGURANÇA (Para o Handler) --- +auth.unauthorized=Full authentication is required to access this resource. +auth.forbidden=You do not have permission to perform this action. +auth.invalid_token=The provided token is expired or malformed. +auth.login_failed=Invalid credentials. Please try again. + +business.conflict=The operation could not be completed due to a conflict with the current state of the resource. +business.integrity_violation=This record cannot be deleted because it is linked to other data. +business.bad_request=The request could not be understood or was missing required parameters. + +# --- ERROS INTERNOS (GENÉRICOS) --- +internal.error=An unexpected error occurred on our server. Please try again later. +database.connection_error=Could not connect to the data source. \ No newline at end of file diff --git a/backend/src/main/resources/application-development.properties b/backend/src/main/resources/application-development.properties new file mode 100644 index 0000000..0e39f8c --- /dev/null +++ b/backend/src/main/resources/application-development.properties @@ -0,0 +1,9 @@ +# Configurações para ambiente de Desenvolvimento Local (Perfil 'dev') +%dev.quarkus.datasource.db-kind=postgresql +%dev.quarkus.datasource.username=postgres +%dev.quarkus.datasource.password=postgres +%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/meu_db + +%dev.quarkus.mailer.host=smtp.zoho.com +%dev.quarkus.mailer.port=465 +%dev.quarkus.mailer.mock=true diff --git a/backend/src/main/resources/application-test.properties b/backend/src/main/resources/application-test.properties new file mode 100644 index 0000000..d95aff2 --- /dev/null +++ b/backend/src/main/resources/application-test.properties @@ -0,0 +1,7 @@ +# Configurações exclusivas para o ambiente de teste + +# Permite que o Hibernate envie Strings para ENUMs no PostgreSQL (usado com o V999__Test_Casts.sql) +quarkus.datasource.jdbc.additional-jdbc-url-parameters=stringtype=unspecified + +# Inclui as migrações de teste que adicionam os CASTs necessários +quarkus.flyway.locations=db/migration,classpath:db/test-migration diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..8f1c709 --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,72 @@ +# -------------> CONFIGURAÇÕES Hibernate <------------- +quarkus.hibernate-orm.database-generation=none +quarkus.hibernate-orm.statistics=true + +# -------------> CONFIGURAÇÕES Database <------------- +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${DB_USER} +quarkus.datasource.password=${DB_PASSWORD} +quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:5432/${DB_NAME} + +# -------------> CONFIGURAÇÕES Flyway <------------- +quarkus.flyway.migrate-at-start=true + +# -------------> CONFIGURAÇÕES Mailer <------------- +quarkus.mailer.host=${MAIL_HOST} +quarkus.mailer.port=${MAIL_PORT} +quarkus.mailer.tls=true +quarkus.mailer.username=${MAIL_USERNAME} +quarkus.mailer.password=${MAIL_PASSWORD} +quarkus.mailer.from=${MAIL_FROM} +quarkus.mailer.mock=false + +# -------------> CONFIGURAÇÕES JWT <------------- +# Para gerar/assinar o token +smallrye.jwt.sign.key.location=classpath:privateKey.pk8 + +# Para validar o token +mp.jwt.verify.publickey.location=classpath:publicKey.pem +mp.jwt.verify.issuer=https://flima.dev + +# -------------> CONFIGURAÇÕES Kafka <------------- +# Canal de Saída (Emitter) +mp.messaging.outgoing.contact-out.connector=smallrye-kafka +mp.messaging.outgoing.contact-out.topic=contact-messages +mp.messaging.outgoing.contact-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer + +# Canal de Entrada (Consumer) +mp.messaging.incoming.contact-in.connector=smallrye-kafka +mp.messaging.incoming.contact-in.topic=contact-messages +mp.messaging.incoming.contact-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer +mp.messaging.incoming.contact-in.group.id=contact-group + +# Estratégia de falha: retry +mp.messaging.incoming.contact-in.retry=true + +# Número máximo de tentativas +mp.messaging.incoming.contact-in.retry-attempts=3 + +# Intervalo entre as tentativas +mp.messaging.incoming.contact-in.retry-delay=2000 + +# Enviar para DLQ. +mp.messaging.incoming.contact-in.failure-strategy=dead-letter-queue + +# Tópico DLQ +mp.messaging.incoming.contact-in.dead-letter-queue.topic=contact-messages-dlq + +# -------------> CONFIGURAÇÕES HTTP / CORS <------------- +quarkus.http.cors.enabled=true +quarkus.http.cors.origins=http://localhost:5173/, https://flima.dev/ +quarkus.http.cors.methods=GET,PUT,POST,DELETE +quarkus.http.cors.headers=accept, authorization, content-type, x-requested-with +quarkus.http.cors.access-control-max-age=24h + +# -------------> CONFIGURAÇÕES Bucket4j <------------- +quarkus.bucket4j.methods."dev.flima.presentation.rest.auth.AuthResource.login".limits[0].permitted-uses=5 +quarkus.bucket4j.methods."dev.flima.presentation.rest.auth.AuthResource.login".limits[0].period=1m + +# -------------> CONFIGURAÇÕES Prometheus <------------- +quarkus.micrometer.export.prometheus.enabled=true +quarkus.micrometer.binder.http-server.enabled=true +quarkus.micrometer.binder.kafka.enabled=true \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1__Create_Initial_Schema.sql b/backend/src/main/resources/db/migration/V1__Create_Initial_Schema.sql new file mode 100644 index 0000000..7236ea4 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__Create_Initial_Schema.sql @@ -0,0 +1,180 @@ +-- Configurações +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE TYPE section_type_enum AS ENUM ('HOME', 'PROJECTS', 'EXPERIENCE', 'EDUCATION', 'CONTACT'); +CREATE TYPE education_type_enum AS ENUM ('DEGREE', 'CERTIFICATION'); +CREATE TYPE status_message_enum AS ENUM ('REPLIED', 'UNREAD', 'READ'); +CREATE TYPE stack_type_enum AS ENUM ('LANGUAGES', 'DATABASES', 'INFRASTRUCTURE', 'MESSAGING'); +CREATE TYPE role_enum AS ENUM ('OWNER'); + +-- Tabelas +CREATE TABLE contents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + section_type section_type_enum NOT NULL, + title VARCHAR(255) NOT NULL CHECK (trim(title) <> ''), + subtitle VARCHAR(255) NOT NULL CHECK (trim(title) <> '') +); + +CREATE TABLE educations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + education_type education_type_enum NOT NULL, + degree VARCHAR(255) NOT NULL CHECK (trim(degree) <> ''), + title VARCHAR(255) NOT NULL CHECK (trim(title) <> ''), + institution VARCHAR(255) NOT NULL CHECK (trim(institution) <> ''), + period VARCHAR(100) NOT NULL CHECK (trim(period) <> ''), + specialization VARCHAR(255) NOT NULL CHECK (trim(specialization) <> '') +); + +CREATE TABLE education_skills ( + education_id UUID NOT NULL, + skill VARCHAR(150) NOT NULL CHECK (trim(skill) <> ''), + + PRIMARY KEY (education_id, skill), + + CONSTRAINT fk_education_skills + FOREIGN KEY (education_id) + REFERENCES educations (id) + ON DELETE CASCADE +); + +CREATE TABLE education_architectures ( + education_id UUID NOT NULL, + architecture VARCHAR(150) NOT NULL CHECK (trim(architecture) <> ''), + + PRIMARY KEY (education_id, architecture), + + CONSTRAINT fk_education_architectures + FOREIGN KEY (education_id) + REFERENCES educations (id) + ON DELETE CASCADE +); + +CREATE TABLE experiences ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL CHECK (trim(title) <> ''), + company VARCHAR(255) NOT NULL CHECK (trim(company) <> ''), + period VARCHAR(100) NOT NULL CHECK (trim(period) <> ''), + icon VARCHAR(50) NOT NULL CHECK (trim(icon) <> '') +); + +CREATE TABLE experience_bullets ( + experience_id UUID NOT NULL, + bullet VARCHAR(150) NOT NULL CHECK (trim(bullet) <> ''), + + PRIMARY KEY (experience_id, bullet), + + CONSTRAINT fk_experience_bullets + FOREIGN KEY (experience_id) + REFERENCES experiences (id) + ON DELETE CASCADE +); + +CREATE TABLE experience_technologies ( + experience_id UUID NOT NULL, + technology VARCHAR(150) NOT NULL CHECK (trim(technology) <> ''), + + PRIMARY KEY (experience_id, technology), + + CONSTRAINT fk_experience_technologies + FOREIGN KEY (experience_id) + REFERENCES experiences (id) + ON DELETE CASCADE +); + +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(150) NOT NULL CHECK (trim(username) <> ''), + email VARCHAR(255) NOT NULL CHECK (trim(email) <> '') CHECK (position('@' in email) > 1), + subject VARCHAR(255) NOT NULL CHECK (trim(subject) <> ''), + message TEXT NOT NULL CHECK (trim(message) <> ''), + timestamp TIMESTAMP NOT NULL, + status_message status_message_enum NOT NULL +); + +CREATE TABLE projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL CHECK (trim(title) <> ''), + subtitle VARCHAR(255) NOT NULL CHECK (trim(subtitle) <> ''), + description VARCHAR(255) NOT NULL CHECK (trim(description) <> ''), + codeSnippet VARCHAR(200), + icon VARCHAR(50) +); + +CREATE TABLE project_technologies ( + project_id UUID NOT NULL, + technology VARCHAR(150) NOT NULL CHECK (trim(technology) <> ''), + + PRIMARY KEY (project_id, technology), + + CONSTRAINT fk_project_technologies + FOREIGN KEY (project_id) + REFERENCES projects (id) + ON DELETE CASCADE +); + +CREATE TABLE stacks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + stack_type stack_type_enum NOT NULL +); + +CREATE TABLE stack_technologies ( + stack_id UUID NOT NULL, + technology VARCHAR(150) NOT NULL CHECK (trim(technology) <> ''), + + PRIMARY KEY (stack_id, technology), + + CONSTRAINT fk_stack_technologies + FOREIGN KEY (stack_id) + REFERENCES stacks (id) + ON DELETE CASCADE +); + +CREATE TABLE stats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + yearsExperience VARCHAR(10) NOT NULL CHECK (trim(yearsExperience) <> ''), + systemDeployed VARCHAR(10) NOT NULL CHECK (trim(systemDeployed) <> ''), + uptimeSLA VARCHAR(15) NOT NULL CHECK (trim(uptimeSLA) <> ''), + commitsLogged VARCHAR(10) NOT NULL CHECK (trim(commitsLogged) <> ''), + status VARCHAR(10) NOT NULL CHECK (trim(status) <> ''), + objective VARCHAR(10) NOT NULL CHECK (trim(objective) <> '') +); + +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(15) UNIQUE NOT NULL CHECK (trim(username) <> ''), + name VARCHAR(15) NOT NULL CHECK (trim(name) <> ''), + last_name VARCHAR(15) NOT NULL CHECK (trim(last_name) <> ''), + email VARCHAR(255) UNIQUE NOT NULL CHECK (trim(email) <> '') CHECK (position('@' in email) > 1), + role role_enum NOT NULL, + password VARCHAR(255) NOT NULL CHECK (trim(password) <> '') +); + +-- Índices +CREATE INDEX idx_contents_section_type + ON contents (section_type); + +CREATE INDEX idx_educations_education_type + ON educations (education_type); + +CREATE INDEX idx_education_skills_education_id + ON education_skills (education_id); + +CREATE INDEX idx_education_architectures_education_id + ON education_architectures (education_id); + +CREATE INDEX idx_experience_bullets_education_id + ON experience_bullets (experience_id); + +CREATE INDEX idx_experience_technologies_education_id + ON experience_technologies (experience_id); + +CREATE INDEX idx_messages_status_message + ON messages (status_message); + +CREATE INDEX idx_messages_timestamp + ON messages (timestamp); + +CREATE INDEX idx_stacks_stack_type + ON stacks (stack_type); + +CREATE INDEX idx_users_role + ON users (role); \ No newline at end of file diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties new file mode 100644 index 0000000..2cf1a50 --- /dev/null +++ b/backend/src/main/resources/messages.properties @@ -0,0 +1,80 @@ +# --- Mensagens Globais de Validação (Bean Validation) --- +global.id.required=Identifier is required for this operation. +global.field.invalid=The provided value for the field is invalid. +title.not_null_or_empty=Title is required. +subtitle.not_null_or_empty=Subtitle is required. +period.not_null_or_empty=Period is required. +icon.not_null_or_empty=Icon is required. +technologies.not_null_or_empty=Technology is required. +username.not_null=Username is required. +email.not_null=Email is required. +email.invalid=Please provide a valid email address. + +# --- Mensagens para Contents --- +content.not_found=Content record not found with the provided ID. +content.section_type.not_null=Section type is required (e.g., HOME, PROJECTS, EXPERIENCE, EDUCATION, CONTACT). +content.section_content.not_null=The actual content body for the section cannot be empty. + +# --- Mensagens para Educations --- +education.not_found=Education record not found. +education.type_education.not_null=Degree or education type is required. +education.institution.not_null=Educational institution name is required. +education.specialization.not_null=Field of study or specialization is required. +education.skills.not_null=At least one skill must be linked to this education. +education.architectures.not_null=Architectural concepts studied are required. + +# --- Mensagens para Experiences --- +experience.not_found=Experience record not found. +experience.company.not_null=Company name is required. +experience.bullets.not_null=A list of responsibilities (bullets) is required. + +# --- Mensagens para Messages (Contact Form) --- +message.not_found=Message not found. +message.subject.not_null=Subject is required to send a message. +message.message.not_null=Message body cannot be empty. +message.timestamp.not_null=Message timestamp is missing. +message.status_message.not_null=Message status must be defined (e.g., REPLIED, UNREAD, READ). +message.already_replied=This message has already been replied to and cannot be modified. +message.reply_empty=The reply content cannot be empty. +message.invalid_status_for_reply=The message status is invalid for a reply operation. +message.trying.reply="There was a problem trying to reply to the message." + +# --- Mensagens para Projects --- +project.not_found=Project not found. +project.description.not_null=Project description is required. +project.codeSnippet.not_null=A representative code snippet is required for the portfolio. + +# --- Mensagens para Stacks --- +stack.not_found=Stack category not found. +stack.stack_type.not_null=Stack type is required (e.g., LANGUAGES, DATABASES, INFRASTRUCTURE, MESSAGING). + +# --- Mensagens para Stats --- +stat.not_found=Stat record not found. +stat.years_experience.not_null=Years of experience count is required. +stat.system_deployed.not_null=Number of systems deployed is required. +stat.uptime_sla.not_null=Uptime SLA percentage is required. +stat.commits_logged.not_null=Total commits count is required. +stat.status.not_null=System current status is required. +stat.objective.not_null=Professional objective statement is required. + +# --- Mensagens para Users --- +user.not_found=User not found. +user.name.not_null=First name is required. +user.last_name.not_null=Last name is required. +user.role.not_null=User role must be assigned. +user.password.not_null=Password is required. +user.password.weak=Password does not meet the minimum security requirements. + +# --- EXCEÇÕES DE NEGÓCIO E SEGURANÇA (Para o Handler) --- +auth.unauthorized=Full authentication is required to access this resource. +auth.forbidden=You do not have permission to perform this action. +auth.invalid_token=The provided token is expired or malformed. +auth.login_failed=Invalid credentials. Please try again. + +business.conflict=The operation could not be completed due to a conflict with the current state of the resource. +business.integrity_violation=This record cannot be deleted because it is linked to other data. +business.bad_request=The request could not be understood or was missing required parameters. + +# --- ERROS INTERNOS (GENÉRICOS) --- +internal.error=An unexpected error occurred on our server. Please try again later. +database.connection_error=Could not connect to the data source. \ No newline at end of file diff --git a/backend/src/main/resources/privateKey.pem b/backend/src/main/resources/privateKey.pem new file mode 100644 index 0000000..811a934 --- /dev/null +++ b/backend/src/main/resources/privateKey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPqqUqUVpj9mMV +C0FFb1sVvYc6YQEMtEimIVvsKp74vaejBPCaWDzf3hG+NJori5WvVmOw2rO5ebD8 +K847sqkxaU69Z1FgdhypfDe8ELsVtkm1y/GXmO54eQo3oIbCfWwPsgsiQ37FIKtM +K+aQ2gJsIHen6QtbYvfoKGj8k+zkt2aguWTccw5MEW9h3hKUo+J/pBV7nQS3Bzho +xZTr7zAlXKNjstDofMgQ3bIu/Ftp4VX2UR3yw+RKC5t87v+wLge/Ru8ZJlSMwwJX +tHytPD5nKG5FuvD3NacSoOVsMrcixJmGawnD88q6U5jy1R2UMjcsgW6uDV2jNcMn +9wr26NJlAgMBAAECggEAAUBUzIQV52HLE0Ki2d6Ty5q0XBHxWW0j9MgFv4XM78dC +M8s6OA64+3cPDWHQQm4gIuDLfTKRTeF6uiLUuNRvulaG/0EYlUDSFVISCp4F57Uf +X6aFaEEL5oVKwspCy1O3W3AjvRs4y0qkbHMujgNi2UdxO2srvhkMGgEe5tORyNCV +KzCiMURK57tm5hqUe5NjbLcgBKGsSC+65fG3UiZzpfHWOETz7D0Zr5M5aw9iESGY +1WwPf1G4BolKu/8rIvfBbZj2McD7pYumeasjeQedVg3gyl97n69n5K2ke1cy7BCs +LA0fi2WooeHgcSS62aHFr9kMGc5NbufMxLHMqSmu2QKBgQDogjq0GnwEs7Cyr+rt +jzRB4N+osq+vAu/VL38BMHAZHXJdpgvHr/bmlNgo6Ufsbo4CDM+mUTkmqZkt5muH +ptTNtj3gUuhGRSdmGD4J6obBsoiFjmuMwq4B3LZ/Ioo2FDEhuNSiS/Th7dNlYniW +DVZEHazC8JhJO8oH0QrjkfmF+QKBgQDkpeGm76HFKbGeD25gfgeaEfdfKrFs2tFv +PigBcocF6zE4uskPz5pTyqkLmZmt4OUBJtwR6ASIQZ2p/dZ6SV3JKxJoTiZdlSOe +iq6AiwoU5RtGZDMfv9IfiqGqZBL5smPOBIvC4sKfp+PxDlzzRtT7bkzATWQpgBD5 +4PYoNLdazQKBgQCXuCswTTvyIYNDBpIpVFIITwIDZh5H+IWhui2JDB+J8/Il8+0p +78QQML5g6+DYAkg+RDfX0paViQQAtKQkT5P7bFkyIUeaWxPbhiQtelFW4fY+GHJL +1tmPM4QOr+46XbC1zZNLGH+CUhuow7nmSGurZSXPywnEd/RcZ2dQmneVmQKBgEU/ +F7//Avc7UVeVRNBkWtkGZ+yieWmGO7d9E5CcptfcCuQrbYkkTpDh56BjvG80lSZs +Jmol4nmBpcY94h7W6Vhrev+r62KcMFVrmr3DXiJF4KTI49LRvUlgKuX3uOd2Z1OV +fN5g3qCLsDTpLK1g3k/nb8ctqRAIgRusTeCPVphhAoGBALh5+qg7Z2+tDi1aFn84 +V1r4CIkujT6h7AIoIgItVDcMnm/I0LUR4377vRUGq4LRlQQ31QDXff2Oq2iyTZ9F +L1N1ceIdE0oopURZV6pR0alkFZvq6qKKbrGqKwWhEAoR5WMvxXZUNWSwbH+QIplK +4xs5IvFJlnsgCn4H4bP0HF1x +-----END PRIVATE KEY----- diff --git a/backend/src/main/resources/privateKey.pk8 b/backend/src/main/resources/privateKey.pk8 new file mode 100644 index 0000000..811a934 --- /dev/null +++ b/backend/src/main/resources/privateKey.pk8 @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPqqUqUVpj9mMV +C0FFb1sVvYc6YQEMtEimIVvsKp74vaejBPCaWDzf3hG+NJori5WvVmOw2rO5ebD8 +K847sqkxaU69Z1FgdhypfDe8ELsVtkm1y/GXmO54eQo3oIbCfWwPsgsiQ37FIKtM +K+aQ2gJsIHen6QtbYvfoKGj8k+zkt2aguWTccw5MEW9h3hKUo+J/pBV7nQS3Bzho +xZTr7zAlXKNjstDofMgQ3bIu/Ftp4VX2UR3yw+RKC5t87v+wLge/Ru8ZJlSMwwJX +tHytPD5nKG5FuvD3NacSoOVsMrcixJmGawnD88q6U5jy1R2UMjcsgW6uDV2jNcMn +9wr26NJlAgMBAAECggEAAUBUzIQV52HLE0Ki2d6Ty5q0XBHxWW0j9MgFv4XM78dC +M8s6OA64+3cPDWHQQm4gIuDLfTKRTeF6uiLUuNRvulaG/0EYlUDSFVISCp4F57Uf +X6aFaEEL5oVKwspCy1O3W3AjvRs4y0qkbHMujgNi2UdxO2srvhkMGgEe5tORyNCV +KzCiMURK57tm5hqUe5NjbLcgBKGsSC+65fG3UiZzpfHWOETz7D0Zr5M5aw9iESGY +1WwPf1G4BolKu/8rIvfBbZj2McD7pYumeasjeQedVg3gyl97n69n5K2ke1cy7BCs +LA0fi2WooeHgcSS62aHFr9kMGc5NbufMxLHMqSmu2QKBgQDogjq0GnwEs7Cyr+rt +jzRB4N+osq+vAu/VL38BMHAZHXJdpgvHr/bmlNgo6Ufsbo4CDM+mUTkmqZkt5muH +ptTNtj3gUuhGRSdmGD4J6obBsoiFjmuMwq4B3LZ/Ioo2FDEhuNSiS/Th7dNlYniW +DVZEHazC8JhJO8oH0QrjkfmF+QKBgQDkpeGm76HFKbGeD25gfgeaEfdfKrFs2tFv +PigBcocF6zE4uskPz5pTyqkLmZmt4OUBJtwR6ASIQZ2p/dZ6SV3JKxJoTiZdlSOe +iq6AiwoU5RtGZDMfv9IfiqGqZBL5smPOBIvC4sKfp+PxDlzzRtT7bkzATWQpgBD5 +4PYoNLdazQKBgQCXuCswTTvyIYNDBpIpVFIITwIDZh5H+IWhui2JDB+J8/Il8+0p +78QQML5g6+DYAkg+RDfX0paViQQAtKQkT5P7bFkyIUeaWxPbhiQtelFW4fY+GHJL +1tmPM4QOr+46XbC1zZNLGH+CUhuow7nmSGurZSXPywnEd/RcZ2dQmneVmQKBgEU/ +F7//Avc7UVeVRNBkWtkGZ+yieWmGO7d9E5CcptfcCuQrbYkkTpDh56BjvG80lSZs +Jmol4nmBpcY94h7W6Vhrev+r62KcMFVrmr3DXiJF4KTI49LRvUlgKuX3uOd2Z1OV +fN5g3qCLsDTpLK1g3k/nb8ctqRAIgRusTeCPVphhAoGBALh5+qg7Z2+tDi1aFn84 +V1r4CIkujT6h7AIoIgItVDcMnm/I0LUR4377vRUGq4LRlQQ31QDXff2Oq2iyTZ9F +L1N1ceIdE0oopURZV6pR0alkFZvq6qKKbrGqKwWhEAoR5WMvxXZUNWSwbH+QIplK +4xs5IvFJlnsgCn4H4bP0HF1x +-----END PRIVATE KEY----- diff --git a/backend/src/main/resources/psw4j.properties b/backend/src/main/resources/psw4j.properties new file mode 100644 index 0000000..0a6afb1 --- /dev/null +++ b/backend/src/main/resources/psw4j.properties @@ -0,0 +1 @@ +global.salt.length=64 \ No newline at end of file diff --git a/backend/src/main/resources/publicKey.pem b/backend/src/main/resources/publicKey.pem new file mode 100644 index 0000000..3c667a8 --- /dev/null +++ b/backend/src/main/resources/publicKey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6qlKlFaY/ZjFQtBRW9b +Fb2HOmEBDLRIpiFb7Cqe+L2nowTwmlg8394RvjSaK4uVr1ZjsNqzuXmw/CvOO7Kp +MWlOvWdRYHYcqXw3vBC7FbZJtcvxl5jueHkKN6CGwn1sD7ILIkN+xSCrTCvmkNoC +bCB3p+kLW2L36Cho/JPs5LdmoLlk3HMOTBFvYd4SlKPif6QVe50Etwc4aMWU6+8w +JVyjY7LQ6HzIEN2yLvxbaeFV9lEd8sPkSgubfO7/sC4Hv0bvGSZUjMMCV7R8rTw+ +ZyhuRbrw9zWnEqDlbDK3IsSZhmsJw/PKulOY8tUdlDI3LIFurg1dozXDJ/cK9ujS +ZQIDAQAB +-----END PUBLIC KEY-----