Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copie este arquivo para .env e preencha os valores antes de rodar `docker compose up`.

# Gere uma string aleatoria propria (ex: `openssl rand -hex 32`). Nunca reutilize o valor padrao do codigo.
APP_REMEMBER_ME_KEY=

# Senha do banco Postgres usado pelo docker-compose.
POSTGRES_PASSWORD=convive

# URL publica onde a aplicacao vai ficar acessivel (usada em links de e-mail).
APP_BASE_URL=http://localhost:8085

# Credenciais SMTP para envio de e-mails (recuperacao de senha, notificacoes). Opcional para testar localmente.
SMTP_USERNAME=
SMTP_PASSWORD=
APP_CONTACT_TO=
4 changes: 4 additions & 0 deletions Convive/.gitignore → .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ build/
### VS Code ###
.vscode/

### Cursor ###
.cursor/
.cursor/rules/
mvnw.cmd

### DATABASE ###
/data/
Expand Down
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Contribuindo com o Convive

Obrigado pelo interesse em contribuir! O Convive é mantido como projeto open source e self-hosted — issues e PRs são bem-vindos.

## Rodando o projeto localmente

Veja a seção "Quickstart" no [README](README.md) para subir a aplicação via `docker compose up` ou via `./mvnw spring-boot:run` (perfil de desenvolvimento com H2 embutido, sem precisar de Docker).

## Rodando os testes

```bash
cd Convive
./mvnw clean test
```

A suíte roda automaticamente em cada Pull Request via GitHub Actions (`.github/workflows/ci.yml`).

## Abrindo uma issue

Antes de abrir um PR grande, abra uma [issue](https://github.com/dioguit0s/Convive-WebApp/issues/new) descrevendo o problema ou a proposta. Isso evita retrabalho caso a abordagem precise de ajuste antes de virar código.

## Enviando um Pull Request

* Crie um branch a partir da branch principal do repositório, com um nome descritivo (ex: `fix/nome-do-bug`, `feat/nome-da-feature`).
* Mantenha o PR focado em uma mudança por vez — PRs pequenos são revisados mais rápido.
* Descreva o que mudou e por quê; se corrigir um bug, descreva como reproduzi-lo antes da correção.
* Certifique-se de que `./mvnw clean test` passa localmente antes de abrir o PR.

## Código de conduta

Seja respeitoso nas discussões de issues e PRs. Críticas técnicas são bem-vindas; ataques pessoais não.
7 changes: 7 additions & 0 deletions Convive/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
target/
data/
uploads/
.git/
.idea/
*.iml
HELP.md
24 changes: 24 additions & 0 deletions Convive/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
RUN ./mvnw -B dependency:go-offline

COPY src src
RUN ./mvnw -B clean package -DskipTests

FROM eclipse-temurin:21-jre
WORKDIR /app

RUN useradd -r convive \
&& mkdir -p /app/uploads \
&& chown -R convive:convive /app

COPY --from=build /app/target/*.jar app.jar

USER convive
EXPOSE 8085
ENTRYPOINT ["java", "-jar", "app.jar"]
6 changes: 6 additions & 0 deletions Convive/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
<artifactId>h2</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,6 @@ private void seedUsuarios() {
moderador.setStatus("Ativo");
moderadorRepository.save(moderador);

Moderador diogo = new Moderador();
diogo.setNome("Diogo Santos");
diogo.setEmail("diogosantos152005@gmail.com");
diogo.setSenhaHash(passwordEncoder.encode("diogos12"));
diogo.setStatus("Ativo");
moderadorRepository.save(diogo);

criarMorador("Carlos Mendes", "carlos.mendes@email.com", 101, false);
criarMorador("Maria Oliveira", "maria.oliveira@email.com", 205, false);
criarMorador("João Pereira", "joao.pereira@email.com", 302, true);
Expand Down
46 changes: 35 additions & 11 deletions Convive/src/main/java/com/EC6/Convive/Config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.EC6.Convive.Security.CustomUserDetailsService;
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.security.authentication.dao.DaoAuthenticationProvider;
Expand All @@ -19,19 +20,32 @@ public class SecurityConfig {

private final CustomUserDetailsService userDetailsService;

@Value("${spring.h2.console.enabled:false}")
private boolean h2ConsoleEnabled;

@Value("${app.remember-me.key:chave-secreta-unica-do-projeto-convive}")
private String rememberMeKey;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/login", "/features", "/about", "/contact",
"/forgot-password", "/reset-password").permitAll()
.requestMatchers("/css/**", "/js/**", "/img/**", "/h2-console/**").permitAll()
.authorizeHttpRequests(authorize -> {
authorize
.requestMatchers("/", "/login", "/features", "/about", "/contact",
"/privacy", "/terms",
"/forgot-password", "/reset-password").permitAll()
.requestMatchers("/css/**", "/js/**", "/img/**").permitAll();

.requestMatchers("/morador/**").hasAnyRole("MORADOR", "MODERADOR")
.requestMatchers("/moderador/**").hasRole("MODERADOR")
if (h2ConsoleEnabled) {
authorize.requestMatchers("/h2-console/**").permitAll();
}

.anyRequest().authenticated()
)
authorize
.requestMatchers("/morador/**").hasAnyRole("MORADOR", "MODERADOR")
.requestMatchers("/moderador/**").hasRole("MODERADOR")

.anyRequest().authenticated();
})
.formLogin(form -> form
.loginPage("/login")
.usernameParameter("email")
Expand All @@ -40,7 +54,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.permitAll()
)
.rememberMe(remember -> remember
.key("chave-secreta-unica-do-projeto-convive")
.key(rememberMeKey)
.userDetailsService(userDetailsService)
.rememberMeParameter("remember-me")
.tokenValiditySeconds(7 * 24 * 60 * 60)
Expand All @@ -50,8 +64,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"))
.headers(headers -> headers.frameOptions(frame -> frame.disable()));
.csrf(csrf -> {
if (h2ConsoleEnabled) {
csrf.ignoringRequestMatchers("/h2-console/**");
}
})
.headers(headers -> {
if (h2ConsoleEnabled) {
// O console do H2 roda em um iframe; so relaxamos a protecao de clickjacking
// quando ele esta habilitado (nunca em producao).
headers.frameOptions(frame -> frame.disable());
}
});

return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,6 @@ public String criarComunicado(@AuthenticationPrincipal CustomUserDetails userDet
} catch (Exception e) {
redirectAttributes.addFlashAttribute("erro", "Erro ao publicar aviso: " + e.getMessage());
}

comunicadoService.insert(comunicado);
redirectAttributes.addFlashAttribute("sucesso", "Aviso publicado com sucesso!");
} else {
redirectAttributes.addFlashAttribute("erro", "Você não tem permissão para publicar comunicados.");
}
Expand All @@ -79,13 +76,22 @@ public String criarComunicado(@AuthenticationPrincipal CustomUserDetails userDet
}

@PostMapping("/excluir")
public String excluirComunicado(@RequestParam UUID id, org.springframework.web.servlet.mvc.support.RedirectAttributes redirectAttributes) {
try {
comunicadoService.delete(id);
redirectAttributes.addFlashAttribute("sucesso", "Aviso excluído com sucesso.");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("erro", "Erro ao excluir o aviso.");
public String excluirComunicado(@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestParam UUID id,
RedirectAttributes redirectAttributes) {
Usuario usuario = userDetails.getUsuario();

if (usuario instanceof Moderador) {
try {
comunicadoService.delete(id);
redirectAttributes.addFlashAttribute("sucesso", "Aviso excluído com sucesso.");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("erro", "Erro ao excluir o aviso.");
}
} else {
redirectAttributes.addFlashAttribute("erro", "Você não tem permissão para excluir comunicados.");
}

return "redirect:/morador/comunicados";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ public String index() {
@GetMapping("/about")
public String about() {return "public/about";}

@GetMapping("/privacy")
public String privacy() {return "public/privacy";}

@GetMapping("/terms")
public String terms() {return "public/terms";}

}
Original file line number Diff line number Diff line change
Expand Up @@ -195,25 +195,4 @@ private static LocalDateTime[] resolverPeriodoTurno(LocalDate data, String turno
default -> throw new IllegalArgumentException("Turno inválido");
};
}

@PostMapping("/usuarios/{id}/inadimplencia")
public String alterarInadimplenciaUsuario(
@PathVariable UUID id,
@RequestParam boolean inadimplente,
RedirectAttributes redirectAttributes
) {
try {
usuarioService.alterarInadimplencia(id, inadimplente);

if (inadimplente) {
redirectAttributes.addFlashAttribute("sucesso", "Morador marcado como INADIMPLENTE com sucesso.");
} else {
redirectAttributes.addFlashAttribute("sucesso", "Restrição removida. Morador agora está ADIMPLENTE.");
}
} catch (Exception e) {
redirectAttributes.addFlashAttribute("erro", "Erro ao tentar alterar o status do morador.");
}

return "redirect:/moderador/usuarios";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,15 @@ public String registrarNovaOcorrencia(
}

@PostMapping("/excluir")
public String excluirOcorrencia(@RequestParam UUID id, RedirectAttributes redirectAttributes) {
try {
ocorrenciaService.delete(id);
public String excluirOcorrencia(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestParam UUID id,
RedirectAttributes redirectAttributes) {
Usuario usuario = userDetails.getUsuario();
if (ocorrenciaService.deleteForUser(id, usuario)) {
redirectAttributes.addFlashAttribute("sucessoOcorrencia", "Ocorrência eliminada com sucesso.");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("erroOcorrencia", "Erro ao eliminar a ocorrência.");
} else {
redirectAttributes.addFlashAttribute("erroOcorrencia", "Não foi possível eliminar esta ocorrência.");
}
return "redirect:/morador/ocorrencias";
}
Expand Down
4 changes: 2 additions & 2 deletions Convive/src/main/java/com/EC6/Convive/Model/Usuario.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ public abstract class Usuario {
@Column(name = "status")
private String status;

@Column(name = "isInadimplente")
private boolean isInadimplente = true;
@Column(name = "isInadimplente", nullable = false)
private boolean isInadimplente = false;

@Column(name = "fotoPerfil")
private String fotoPerfil;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
public interface OcorrenciaRepository extends JpaRepository<Ocorrencia, UUID> {
List<Ocorrencia> findByUsuarioId(UUID moradorId);

Optional<Ocorrencia> findByIdAndUsuario_Id(UUID id, UUID usuarioId);

Page<Ocorrencia> findByUsuarioIdOrderByDataRegistroDesc(UUID moradorId, Pageable pageable);

@Query("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public Morador update(UUID id, Morador moradorAtualizado) {
moradorExistente.setNome(moradorAtualizado.getNome());
moradorExistente.setEmail(moradorAtualizado.getEmail());
moradorExistente.setApartamento(moradorAtualizado.getApartamento());
moradorExistente.setInadimplente(moradorAtualizado.isInadimplente());

return moradorRepository.save(moradorExistente);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.EC6.Convive.Service;

import com.EC6.Convive.Event.OcorrenciaCriadaEvent;
import com.EC6.Convive.Model.Moderador;
import com.EC6.Convive.Model.Ocorrencia;
import com.EC6.Convive.Model.Prioridade;
import com.EC6.Convive.Model.StatusOcorrencia;
import com.EC6.Convive.Model.Usuario;
import com.EC6.Convive.Repository.OcorrenciaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
Expand Down Expand Up @@ -83,6 +85,20 @@ public void delete(UUID id) {
ocorrenciaRepository.deleteById(id);
}

public boolean deleteForUser(UUID ocorrenciaId, Usuario usuario) {
Optional<Ocorrencia> ocorrencia;
if (usuario instanceof Moderador) {
ocorrencia = ocorrenciaRepository.findById(ocorrenciaId);
} else {
ocorrencia = ocorrenciaRepository.findByIdAndUsuario_Id(ocorrenciaId, usuario.getId());
}
if (ocorrencia.isEmpty()) {
return false;
}
ocorrenciaRepository.delete(ocorrencia.get());
return true;
}

public List<Ocorrencia> listByUser(UUID moradorId) {
return ocorrenciaRepository.findByUsuarioId(moradorId);
}
Expand Down
10 changes: 0 additions & 10 deletions Convive/src/main/java/com/EC6/Convive/Service/UsuarioService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;
Expand Down Expand Up @@ -52,15 +51,6 @@ public void delete(UUID id) {
usuarioRepository.deleteById(id);
}

@Transactional
public void alterarInadimplencia(UUID usuarioId, boolean status) {
Usuario usuario = usuarioRepository.findById(usuarioId)
.orElseThrow(() -> new RuntimeException("Usuário não encontrado com o ID: " + usuarioId));

usuario.setInadimplente(status);
usuarioRepository.save(usuario);
}

public Usuario update(UUID id, Usuario moradorAtualizado) {
Usuario usuario = searchById(id);

Expand Down
15 changes: 15 additions & 0 deletions Convive/src/main/resources/application-prod.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Perfil de producao/self-hosting: Postgres real, sem H2 console exposto.
# Ativar com SPRING_PROFILES_ACTIVE=prod (ja definido no docker-compose.yml).

spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://db:5432/convive}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:convive}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:convive}
spring.datasource.driver-class-name=org.postgresql.Driver

spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update

spring.h2.console.enabled=false

# Defina uma chave propria e secreta em producao (ex: via variavel de ambiente APP_REMEMBER_ME_KEY).
app.remember-me.key=${APP_REMEMBER_ME_KEY:chave-secreta-unica-do-projeto-convive}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading