From ef20162351886cae6e07ca2ecb082070073e1ac6 Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 19 May 2026 20:52:36 -0300 Subject: [PATCH] =?UTF-8?q?Corre=C3=A7=C3=A3o=20de=20bugs=20e=20cria=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20termos=20de=20privacidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 37 + Dockerfile | 28 + README.md | 133 ++ docker-compose.yml | 22 + pom.xml | 141 ++ .../AgendaDigitalEstudantesApplication.java | 14 + .../configuracao/ConfiguracaoMongo.java | 9 + .../configuracao/ConfiguracaoSeguranca.java | 101 ++ .../configuracao/ResourceController.java | 23 + .../controlador/DisciplinaControlador.java | 83 + .../controlador/EstudanteControlador.java | 75 + .../controlador/EventoControlador.java | 97 ++ .../controlador/NotificacaoControlador.java | 68 + .../controlador/TarefaControlador.java | 92 ++ .../RequisicaoAtualizacaoEstudanteDTO.java | 12 + .../dto/RequisicaoCadastroDTO.java | 17 + .../dto/RequisicaoDisciplinaDTO.java | 10 + .../dto/RequisicaoEventoDTO.java | 16 + .../dto/RequisicaoLoginDTO.java | 10 + .../dto/RequisicaoTarefaDTO.java | 18 + .../dto/RequisicaoTrocaSenhaDTO.java | 10 + .../com/agendaestudantil/dto/RespostaApi.java | 9 + .../dto/RespostaDadosCompletoDTO.java | 12 + .../dto/RespostaDisciplinaDTO.java | 10 + .../dto/RespostaEstudanteDTO.java | 10 + .../dto/RespostaEventoDTO.java | 16 + .../dto/RespostaLoginDTO.java | 4 + .../dto/RespostaNotificacaoDTO.java | 14 + .../dto/RespostaTarefaDTO.java | 14 + .../agendaestudantil/entidade/Disciplina.java | 30 + .../entidade/EntidadeAuditoria.java | 21 + .../agendaestudantil/entidade/Estudante.java | 41 + .../com/agendaestudantil/entidade/Evento.java | 43 + .../entidade/Notificacao.java | 43 + .../com/agendaestudantil/entidade/Tarefa.java | 46 + .../excecao/ExcecaoNegocio.java | 7 + .../excecao/ExcecaoRecursoNaoEncontrado.java | 7 + .../excecao/ManipuladorExcecaoGlobal.java | 62 + .../agendaestudantil/filtro/FiltroJwt.java | 62 + .../repositorio/DisciplinaRepositorio.java | 16 + .../repositorio/EstudanteRepositorio.java | 12 + .../repositorio/EventoRepositorio.java | 27 + .../repositorio/NotificacaoRepositorio.java | 25 + .../repositorio/TarefaRepositorio.java | 32 + .../DetalhesUsuarioPersonalizado.java | 53 + .../seguranca/ServicoAutenticacaoUsuario.java | 26 + .../servico/DisciplinaServico.java | 107 ++ .../servico/EstudanteServico.java | 184 +++ .../servico/EventoServico.java | 170 +++ .../servico/NotificacaoAgendadorServico.java | 132 ++ .../servico/NotificacaoServico.java | 98 ++ .../servico/TarefaServico.java | 143 ++ .../agendaestudantil/utilitario/UtilJwt.java | 58 + src/main/resources/application-dev.properties | 7 + .../resources/application-prod.properties | 8 + src/main/resources/application.properties | 13 + src/main/resources/static/cadastro.css | 182 +++ src/main/resources/static/cadastro.html | 174 +++ src/main/resources/static/calendario.css | 519 +++++++ src/main/resources/static/calendario.html | 1329 +++++++++++++++++ src/main/resources/static/configuracoes.css | 333 +++++ src/main/resources/static/configuracoes.html | 323 ++++ .../resources/static/imagens/engrenagem.png | Bin 0 -> 13977 bytes src/main/resources/static/imagens/icone.png | Bin 0 -> 7127 bytes .../static/imagens/moon-svgrepo-com.svg | 4 + src/main/resources/static/imagens/sino.png | Bin 0 -> 8791 bytes .../static/imagens/sun-svgrepo-com.svg | 4 + src/main/resources/static/index.html | 11 + src/main/resources/static/login.css | 126 ++ src/main/resources/static/login.html | 104 ++ .../static/politica-privacidade.html | 119 ++ src/main/resources/static/style.css | 124 ++ src/main/resources/static/theme.js | 36 + src/main/resources/static/utils.js | 83 + .../servico/EstudanteServicoTest.java | 107 ++ .../servico/TarefaServicoTest.java | 130 ++ 76 files changed, 6286 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 pom.xml create mode 100644 src/main/java/com/agendaestudantil/AgendaDigitalEstudantesApplication.java create mode 100644 src/main/java/com/agendaestudantil/configuracao/ConfiguracaoMongo.java create mode 100644 src/main/java/com/agendaestudantil/configuracao/ConfiguracaoSeguranca.java create mode 100644 src/main/java/com/agendaestudantil/configuracao/ResourceController.java create mode 100644 src/main/java/com/agendaestudantil/controlador/DisciplinaControlador.java create mode 100644 src/main/java/com/agendaestudantil/controlador/EstudanteControlador.java create mode 100644 src/main/java/com/agendaestudantil/controlador/EventoControlador.java create mode 100644 src/main/java/com/agendaestudantil/controlador/NotificacaoControlador.java create mode 100644 src/main/java/com/agendaestudantil/controlador/TarefaControlador.java create mode 100644 src/main/java/com/agendaestudantil/dto/RequisicaoAtualizacaoEstudanteDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RequisicaoCadastroDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RequisicaoDisciplinaDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RequisicaoEventoDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RequisicaoLoginDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RequisicaoTarefaDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RequisicaoTrocaSenhaDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RespostaApi.java create mode 100644 src/main/java/com/agendaestudantil/dto/RespostaDadosCompletoDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RespostaDisciplinaDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RespostaEstudanteDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RespostaEventoDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RespostaLoginDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RespostaNotificacaoDTO.java create mode 100644 src/main/java/com/agendaestudantil/dto/RespostaTarefaDTO.java create mode 100644 src/main/java/com/agendaestudantil/entidade/Disciplina.java create mode 100644 src/main/java/com/agendaestudantil/entidade/EntidadeAuditoria.java create mode 100644 src/main/java/com/agendaestudantil/entidade/Estudante.java create mode 100644 src/main/java/com/agendaestudantil/entidade/Evento.java create mode 100644 src/main/java/com/agendaestudantil/entidade/Notificacao.java create mode 100644 src/main/java/com/agendaestudantil/entidade/Tarefa.java create mode 100644 src/main/java/com/agendaestudantil/excecao/ExcecaoNegocio.java create mode 100644 src/main/java/com/agendaestudantil/excecao/ExcecaoRecursoNaoEncontrado.java create mode 100644 src/main/java/com/agendaestudantil/excecao/ManipuladorExcecaoGlobal.java create mode 100644 src/main/java/com/agendaestudantil/filtro/FiltroJwt.java create mode 100644 src/main/java/com/agendaestudantil/repositorio/DisciplinaRepositorio.java create mode 100644 src/main/java/com/agendaestudantil/repositorio/EstudanteRepositorio.java create mode 100644 src/main/java/com/agendaestudantil/repositorio/EventoRepositorio.java create mode 100644 src/main/java/com/agendaestudantil/repositorio/NotificacaoRepositorio.java create mode 100644 src/main/java/com/agendaestudantil/repositorio/TarefaRepositorio.java create mode 100644 src/main/java/com/agendaestudantil/seguranca/DetalhesUsuarioPersonalizado.java create mode 100644 src/main/java/com/agendaestudantil/seguranca/ServicoAutenticacaoUsuario.java create mode 100644 src/main/java/com/agendaestudantil/servico/DisciplinaServico.java create mode 100644 src/main/java/com/agendaestudantil/servico/EstudanteServico.java create mode 100644 src/main/java/com/agendaestudantil/servico/EventoServico.java create mode 100644 src/main/java/com/agendaestudantil/servico/NotificacaoAgendadorServico.java create mode 100644 src/main/java/com/agendaestudantil/servico/NotificacaoServico.java create mode 100644 src/main/java/com/agendaestudantil/servico/TarefaServico.java create mode 100644 src/main/java/com/agendaestudantil/utilitario/UtilJwt.java create mode 100644 src/main/resources/application-dev.properties create mode 100644 src/main/resources/application-prod.properties create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/static/cadastro.css create mode 100644 src/main/resources/static/cadastro.html create mode 100644 src/main/resources/static/calendario.css create mode 100644 src/main/resources/static/calendario.html create mode 100644 src/main/resources/static/configuracoes.css create mode 100644 src/main/resources/static/configuracoes.html create mode 100644 src/main/resources/static/imagens/engrenagem.png create mode 100644 src/main/resources/static/imagens/icone.png create mode 100644 src/main/resources/static/imagens/moon-svgrepo-com.svg create mode 100644 src/main/resources/static/imagens/sino.png create mode 100644 src/main/resources/static/imagens/sun-svgrepo-com.svg create mode 100644 src/main/resources/static/index.html create mode 100644 src/main/resources/static/login.css create mode 100644 src/main/resources/static/login.html create mode 100644 src/main/resources/static/politica-privacidade.html create mode 100644 src/main/resources/static/style.css create mode 100644 src/main/resources/static/theme.js create mode 100644 src/main/resources/static/utils.js create mode 100644 src/test/java/com/agendaestudantil/servico/EstudanteServicoTest.java create mode 100644 src/test/java/com/agendaestudantil/servico/TarefaServicoTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a900d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Eclipse +.settings/ +.classpath +.project + +# IntelliJ IDEA +.idea/ +*.iws +*.iml +*.ipr +out/ +*.factorypath + +# VS Code +.vscode/ + +# Environment variables +.env + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..835cbca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM maven:3.9-eclipse-temurin-17 AS build + +WORKDIR /app + +COPY pom.xml . +RUN mvn dependency:go-offline -B + +COPY src ./src +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:17-jre-alpine + +WORKDIR /app + +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +COPY --from=build /app/target/*.jar app.jar + +RUN chown appuser:appgroup app.jar + +USER appuser + +EXPOSE 8080 + +ENV SPRING_PROFILES_ACTIVE=prod +ENV SERVER_PORT=8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..955e20b --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# Focus Agenda - Agenda Digital para Estudantes + +CENTRO ESTADUAL DE EDUCACAO TECNOLOGICA "PAULA SOUZA" +ETEC PEDRO D'ARCADIA NETO +Tecnico em Desenvolvimento de Sistemas + +## Autores + +- BORGES, Gabriel H. M. +- CRUZ, Fernando M. B. da +- ARAUJO, Gustavo Ferreira +- OLIVEIRA, Henry E. de +- HABU, Nadia Sakae + +##Descricao + +Plataforma digital para organizacao de estudos destinada a alunos do ensino medio e tecnico. A ferramenta auxilia na gestao de rotinas academicas, enviando notificacoes sobre atividades diarias, horarios de estudo, datas de provas e outros compromissos academicos. + +## Funcionalidades + +- Cadastro e autenticacao de usuarios +- Calendario mensal, semanal e diario +- Criacao e gerenciamentos de eventos +- Criacao e gerenciamento de tarefas com prioridades +- Gerenciamento de disciplinas +- Sistema de notificacoes +- Tema claro e escuro +- Painel informativo com feriados nacionais + +## Tecnologias + +### Frontend +- HTML5 +- CSS3 +- JavaScript + +### Backend +- Java 17 +- Spring Boot 3.2.0 +- Spring Security +- JWT (JSON Web Token) +- MongoDB + +## Requisitos + +- Java 17 ou superior +- Maven 3.8+ +- MongoDB + +## Execucao + +### Build do projeto + +```bash +mvn clean package +``` + +### Execucao com Maven + +```bash +mvn spring-boot:run +``` + +### Execucao com JAR + +```bash +java -jar target/agenda-digital-estudantes-1.0.0.jar +``` + +### Variaveis de ambiente + +| Variavel | Descricao | Valor padrao | +|---|---|---| +| APP_NAME | Nome da aplicacao | Focus Agenda | +| SERVER_PORT | Porta do servidor | 8080 | +| SPRING_PROFILES_ACTIVE | Perfil ativo | dev | +| MONGO_URI | URI de conexao com MongoDB | mongodb://localhost:27017/agenda_estudantil | +| CORS_ORIGINS | Origens permitidas para CORS | http://localhost:8080,http://localhost:3000 | +| JWT_SECRET | Chave secreta para JWT | (chave padrao) | +| JWT_EXPIRATION | Expiracao do token em milissegundos | 86400000 | + +## Docker + +### Build da imagem + +```bash +docker build -t focus-agenda . +``` + +### Execucao do container + +```bash +docker run -d -p 8080:8080 --name focus-agenda focus-agenda +``` + +### Execucao com MongoDB + +```bash +docker run -d -p 8080:8080 -e MONGO_URI=mongodb://host.docker.internal:27017/agenda_estudantil --name focus-agenda focus-agenda +``` + +## Acesso + +Apos iniciar a aplicacao, acesse: + +- Aplicacao: http://localhost:8080 +- Swagger UI: http://localhost:8080/swagger-ui.html +- API Docs: http://localhost:8080/v3/api-docs + +## Estrutura do Projeto + +``` +src/ +├── main/ +│ ├── java/com/agendaestudantil/ +│ │ ├── configuracao/ # Configuracoes de seguranca e MongoDB +│ │ ├── controlador/ # Controladores REST +│ │ ├── dto/ # Objetos de transferencia de dados +│ │ ├── entidade/ # Entidades do dominio +│ │ ├── excecao/ # Excecoes personalizadas e manipulador global +│ │ ├── filtro/ # Filtro JWT +│ │ ├── repositorio/ # Interfaces de repositorio +│ │ ├── seguranca/ # Autenticacao e detalhes do usuario +│ │ ├── servico/ # Regras de negocio +│ │ └── utilitario/ # Utilitarios (JWT) +│ └── resources/ +│ ├── static/ # Frontend (HTML, CSS, JS) +│ └── application*.properties +``` + +## Licenca + +Projeto academico desenvolvido para o Curso Tecnico em Desenvolvimento de Sistemas da ETEC Pedro D'Arcadia Neto. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..748a298 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + mongodb: + image: mongo:7 + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db + + app: + build: . + ports: + - "8080:8080" + depends_on: + - mongodb + environment: + MONGO_URI: mongodb://mongodb:27017/agenda_estudantil + JWT_SECRET: 4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b + CORS_ORIGINS: http://localhost:8080 + SPRING_PROFILES_ACTIVE: prod + +volumes: + mongo_data: diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5853075 --- /dev/null +++ b/pom.xml @@ -0,0 +1,141 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.agendaestudantil + agenda-digital-estudantes + 1.0.0 + Agenda Digital para Estudantes + Backend para agenda digital destinado a estudantes com dificuldade de organização + + + 17 + 17 + 17 + 1.18.30 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + org.springframework.boot + spring-boot-starter-validation + + + + + + org.springframework.boot + spring-boot-starter-security + + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + io.github.cdimascio + dotenv-java + 2.3.2 + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.mockito + mockito-core + test + + + + org.springframework.security + spring-security-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + -parameters + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/src/main/java/com/agendaestudantil/AgendaDigitalEstudantesApplication.java b/src/main/java/com/agendaestudantil/AgendaDigitalEstudantesApplication.java new file mode 100644 index 0000000..16ed183 --- /dev/null +++ b/src/main/java/com/agendaestudantil/AgendaDigitalEstudantesApplication.java @@ -0,0 +1,14 @@ +package com.agendaestudantil; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class AgendaDigitalEstudantesApplication { + + public static void main(String[] args) { + SpringApplication.run(AgendaDigitalEstudantesApplication.class, args); + } +} diff --git a/src/main/java/com/agendaestudantil/configuracao/ConfiguracaoMongo.java b/src/main/java/com/agendaestudantil/configuracao/ConfiguracaoMongo.java new file mode 100644 index 0000000..3f5f981 --- /dev/null +++ b/src/main/java/com/agendaestudantil/configuracao/ConfiguracaoMongo.java @@ -0,0 +1,9 @@ +package com.agendaestudantil.configuracao; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; + +@Configuration +@EnableMongoAuditing +public class ConfiguracaoMongo { +} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/configuracao/ConfiguracaoSeguranca.java b/src/main/java/com/agendaestudantil/configuracao/ConfiguracaoSeguranca.java new file mode 100644 index 0000000..df6f259 --- /dev/null +++ b/src/main/java/com/agendaestudantil/configuracao/ConfiguracaoSeguranca.java @@ -0,0 +1,101 @@ +package com.agendaestudantil.configuracao; + +import com.agendaestudantil.dto.RespostaApi; +import com.agendaestudantil.filtro.FiltroJwt; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationManager; +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.http.SessionCreationPolicy; +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.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Configuration +@EnableWebSecurity +public class ConfiguracaoSeguranca { + + private final FiltroJwt filtroJwt; + private final String corsAllowedOrigins; + private final String perfilAtivo; + + public ConfiguracaoSeguranca(FiltroJwt filtroJwt, + @Value("${cors.allowed.origins}") String corsAllowedOrigins, + @Value("${spring.profiles.active:dev}") String perfilAtivo) { + this.filtroJwt = filtroJwt; + this.corsAllowedOrigins = corsAllowedOrigins; + this.perfilAtivo = perfilAtivo; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + List pathsPublicos = new ArrayList<>(List.of( + "/", "/index.html", "/login.html", "/cadastro.html", "/calendario.html", "/configuracoes.html", + "/politica-privacidade.html", + "/favicon.ico", "/imagens/**", + "/*.css", "/*.js", "/*.ico", "/*.png", + "/api/estudantes/cadastro", "/api/estudantes/login")); + + if ("dev".equals(perfilAtivo)) { + pathsPublicos.add("/swagger-ui/**"); + pathsPublicos.add("/v3/api-docs/**"); + } + + http.csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(auth -> auth + .requestMatchers(pathsPublicos.toArray(new String[0])) + .permitAll() + .anyRequest().authenticated()) + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + RespostaApi body = new RespostaApi<>(null, "Acesso nao autorizado", LocalDateTime.now()); + new ObjectMapper().writeValue(response.getOutputStream(), body); + })) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(filtroJwt, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of(corsAllowedOrigins.split(","))); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("Authorization")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/agendaestudantil/configuracao/ResourceController.java b/src/main/java/com/agendaestudantil/configuracao/ResourceController.java new file mode 100644 index 0000000..46321c3 --- /dev/null +++ b/src/main/java/com/agendaestudantil/configuracao/ResourceController.java @@ -0,0 +1,23 @@ +package com.agendaestudantil.configuracao; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class ResourceController { + + @GetMapping("/") + public String index() { + return "forward:/login.html"; + } + + @GetMapping("/app") + public String app() { + return "forward:/calendario.html"; + } + + @GetMapping("/config") + public String config() { + return "forward:/configuracoes.html"; + } +} diff --git a/src/main/java/com/agendaestudantil/controlador/DisciplinaControlador.java b/src/main/java/com/agendaestudantil/controlador/DisciplinaControlador.java new file mode 100644 index 0000000..ae98559 --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/DisciplinaControlador.java @@ -0,0 +1,83 @@ +package com.agendaestudantil.controlador; + +import com.agendaestudantil.dto.RequisicaoDisciplinaDTO; +import com.agendaestudantil.dto.RespostaApi; +import com.agendaestudantil.dto.RespostaDisciplinaDTO; +import com.agendaestudantil.entidade.Disciplina; +import com.agendaestudantil.servico.DisciplinaServico; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/disciplinas") +public class DisciplinaControlador { + + private final DisciplinaServico disciplinaServico; + + public DisciplinaControlador(DisciplinaServico disciplinaServico) { + this.disciplinaServico = disciplinaServico; + } + + @PostMapping + public ResponseEntity> criarDisciplina( + @Valid @RequestBody RequisicaoDisciplinaDTO dto, + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + Disciplina disciplina = new Disciplina(); + disciplina.setNome(dto.nome()); + disciplina.setProfessor(dto.professor()); + disciplina.setSala(dto.sala()); + disciplina.setCor(dto.cor()); + + RespostaDisciplinaDTO resposta = disciplinaServico.criarDisciplina(disciplina, estudanteId); + return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta)); + } + + @GetMapping("/me") + public ResponseEntity>> listarPorEstudante( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + List disciplinas = disciplinaServico.listarPorEstudante(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(disciplinas)); + } + + @GetMapping("/{id}") + public ResponseEntity> buscarPorId( + @PathVariable String id, + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + RespostaDisciplinaDTO disciplina = disciplinaServico.buscarPorId(id, estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(disciplina)); + } + + @PutMapping("/{id}") + public ResponseEntity> atualizarDisciplina( + @PathVariable String id, + @Valid @RequestBody RequisicaoDisciplinaDTO dto, + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + Disciplina disciplina = new Disciplina(); + disciplina.setNome(dto.nome()); + disciplina.setProfessor(dto.professor()); + disciplina.setSala(dto.sala()); + disciplina.setCor(dto.cor()); + + RespostaDisciplinaDTO resposta = disciplinaServico.atualizarDisciplina(id, disciplina, estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(resposta)); + } + + @DeleteMapping("/{id}") + public ResponseEntity> excluirDisciplina( + @PathVariable String id, + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + disciplinaServico.excluirDisciplina(id, estudanteId); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null)); + } +} diff --git a/src/main/java/com/agendaestudantil/controlador/EstudanteControlador.java b/src/main/java/com/agendaestudantil/controlador/EstudanteControlador.java new file mode 100644 index 0000000..e72b11d --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/EstudanteControlador.java @@ -0,0 +1,75 @@ +package com.agendaestudantil.controlador; + +import com.agendaestudantil.dto.RespostaApi; +import com.agendaestudantil.dto.RequisicaoAtualizacaoEstudanteDTO; +import com.agendaestudantil.dto.RequisicaoCadastroDTO; +import com.agendaestudantil.dto.RespostaDadosCompletoDTO; +import com.agendaestudantil.dto.RespostaEstudanteDTO; +import com.agendaestudantil.dto.RequisicaoLoginDTO; +import com.agendaestudantil.dto.RespostaLoginDTO; +import com.agendaestudantil.dto.RequisicaoTrocaSenhaDTO; +import com.agendaestudantil.servico.EstudanteServico; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/estudantes") +public class EstudanteControlador { + + private final EstudanteServico estudanteServico; + + public EstudanteControlador(EstudanteServico estudanteServico) { + this.estudanteServico = estudanteServico; + } + + @PostMapping("/cadastro") + public ResponseEntity> cadastrar(@Valid @RequestBody RequisicaoCadastroDTO dto) { + RespostaEstudanteDTO resposta = estudanteServico.cadastrar(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta)); + } + + @PostMapping("/login") + public ResponseEntity> login(@Valid @RequestBody RequisicaoLoginDTO dto) { + RespostaLoginDTO resposta = estudanteServico.login(dto); + return ResponseEntity.ok(RespostaApi.sucesso(resposta)); + } + + @GetMapping("/me") + public ResponseEntity> me(@AuthenticationPrincipal UserDetails userDetails) { + RespostaEstudanteDTO resposta = estudanteServico.buscarPorId(userDetails.getUsername()); + return ResponseEntity.ok(RespostaApi.sucesso(resposta)); + } + + @GetMapping("/me/dados") + public ResponseEntity> exportarDados( + @AuthenticationPrincipal UserDetails userDetails) { + RespostaDadosCompletoDTO dados = estudanteServico.exportarDados(userDetails.getUsername()); + return ResponseEntity.ok(RespostaApi.sucesso(dados)); + } + + @PutMapping("/me") + public ResponseEntity> atualizar( + @AuthenticationPrincipal UserDetails userDetails, + @Valid @RequestBody RequisicaoAtualizacaoEstudanteDTO dto) { + RespostaEstudanteDTO resposta = estudanteServico.atualizar(userDetails.getUsername(), dto); + return ResponseEntity.ok(RespostaApi.sucesso(resposta)); + } + + @PutMapping("/senha") + public ResponseEntity> trocarSenha( + @AuthenticationPrincipal UserDetails userDetails, + @Valid @RequestBody RequisicaoTrocaSenhaDTO dto) { + estudanteServico.trocarSenha(userDetails.getUsername(), dto); + return ResponseEntity.ok(RespostaApi.sucesso(null)); + } + + @DeleteMapping("/me") + public ResponseEntity> excluirConta(@AuthenticationPrincipal UserDetails userDetails) { + estudanteServico.excluirConta(userDetails.getUsername()); + return ResponseEntity.ok(RespostaApi.sucesso(null)); + } +} diff --git a/src/main/java/com/agendaestudantil/controlador/EventoControlador.java b/src/main/java/com/agendaestudantil/controlador/EventoControlador.java new file mode 100644 index 0000000..b1f6363 --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/EventoControlador.java @@ -0,0 +1,97 @@ +package com.agendaestudantil.controlador; + +import com.agendaestudantil.dto.RequisicaoEventoDTO; +import com.agendaestudantil.dto.RespostaApi; +import com.agendaestudantil.dto.RespostaEventoDTO; +import com.agendaestudantil.entidade.Evento; +import com.agendaestudantil.servico.EventoServico; +import jakarta.validation.Valid; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/api/eventos") +public class EventoControlador { + + private final EventoServico eventoServico; + + public EventoControlador(EventoServico eventoServico) { + this.eventoServico = eventoServico; + } + + @PostMapping + public ResponseEntity> criarEvento( + @Valid @RequestBody RequisicaoEventoDTO dto, + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + Evento evento = new Evento(); + evento.setTitulo(dto.titulo()); + evento.setDescricao(dto.descricao()); + evento.setTipo(dto.tipo()); + evento.setLocal(dto.local()); + evento.setDataHora(dto.dataHora()); + evento.setDisciplinaId(dto.disciplinaId()); + + RespostaEventoDTO resposta = eventoServico.criarEvento(evento, estudanteId); + return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta)); + } + + @GetMapping("/me") + public ResponseEntity>> listarPorEstudante( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + List eventos = eventoServico.listarPorEstudante(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(eventos)); + } + + @GetMapping("/me/periodo") + public ResponseEntity>> listarPorPeriodo( + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime inicio, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime fim) { + String estudanteId = userDetails.getUsername(); + List eventos = eventoServico.listarPorPeriodo(estudanteId, inicio, fim); + return ResponseEntity.ok(RespostaApi.sucesso(eventos)); + } + + @GetMapping("/me/proximos") + public ResponseEntity>> listarProximosEventos( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + List eventos = eventoServico.listarProximosEventos(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(eventos)); + } + + @GetMapping("/{id}") + public ResponseEntity> buscarPorId(@PathVariable String id) { + RespostaEventoDTO evento = eventoServico.buscarPorId(id); + return ResponseEntity.ok(RespostaApi.sucesso(evento)); + } + + @PutMapping("/{id}") + public ResponseEntity> atualizarEvento( + @PathVariable String id, + @Valid @RequestBody RequisicaoEventoDTO dto) { + RespostaEventoDTO resposta = eventoServico.atualizarEvento(id, dto); + return ResponseEntity.ok(RespostaApi.sucesso(resposta)); + } + + @DeleteMapping("/{id}") + public ResponseEntity> excluirEvento(@PathVariable String id) { + eventoServico.excluirEvento(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null)); + } + + @PatchMapping("/{id}/cancelar") + public ResponseEntity> cancelarEvento(@PathVariable String id) { + RespostaEventoDTO evento = eventoServico.cancelarEvento(id); + return ResponseEntity.ok(RespostaApi.sucesso(evento)); + } +} diff --git a/src/main/java/com/agendaestudantil/controlador/NotificacaoControlador.java b/src/main/java/com/agendaestudantil/controlador/NotificacaoControlador.java new file mode 100644 index 0000000..f5a3dff --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/NotificacaoControlador.java @@ -0,0 +1,68 @@ +package com.agendaestudantil.controlador; + +import com.agendaestudantil.dto.RespostaApi; +import com.agendaestudantil.dto.RespostaNotificacaoDTO; +import com.agendaestudantil.servico.NotificacaoServico; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/notificacoes") +public class NotificacaoControlador { + + private final NotificacaoServico notificacaoServico; + + public NotificacaoControlador(NotificacaoServico notificacaoServico) { + this.notificacaoServico = notificacaoServico; + } + + @GetMapping("/me") + public ResponseEntity>> listarTodas( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + List notificacoes = notificacaoServico.listarTodas(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(notificacoes)); + } + + @GetMapping("/me/nao-lidas") + public ResponseEntity>> listarNaoLidas( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + List notificacoes = notificacaoServico.listarNaoLidas(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(notificacoes)); + } + + @GetMapping("/me/contagem") + public ResponseEntity>> contarNaoLidas( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + long total = notificacaoServico.contarNaoLidas(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(Map.of("total", total))); + } + + @PatchMapping("/{id}/ler") + public ResponseEntity> marcarComoLida(@PathVariable String id) { + RespostaNotificacaoDTO notificacao = notificacaoServico.marcarComoLida(id); + return ResponseEntity.ok(RespostaApi.sucesso(notificacao)); + } + + @PatchMapping("/me/ler-todas") + public ResponseEntity> marcarTodasComoLidas( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + notificacaoServico.marcarTodasComoLidas(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(null)); + } + + @DeleteMapping("/{id}") + public ResponseEntity> excluirNotificacao(@PathVariable String id) { + notificacaoServico.excluirNotificacao(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null)); + } +} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/controlador/TarefaControlador.java b/src/main/java/com/agendaestudantil/controlador/TarefaControlador.java new file mode 100644 index 0000000..c1a93d8 --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/TarefaControlador.java @@ -0,0 +1,92 @@ +package com.agendaestudantil.controlador; + +import com.agendaestudantil.dto.RespostaApi; +import com.agendaestudantil.dto.RequisicaoTarefaDTO; +import com.agendaestudantil.dto.RespostaTarefaDTO; +import com.agendaestudantil.servico.TarefaServico; +import jakarta.validation.Valid; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/api/tarefas") +public class TarefaControlador { + + private final TarefaServico tarefaServico; + + public TarefaControlador(TarefaServico tarefaServico) { + this.tarefaServico = tarefaServico; + } + + @PostMapping + public ResponseEntity> criarTarefa(@Valid @RequestBody RequisicaoTarefaDTO dto) { + RespostaTarefaDTO tarefa = tarefaServico.criarTarefa(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(tarefa)); + } + + @GetMapping("/me") + public ResponseEntity>> listarTarefasPorEstudante( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + List tarefas = tarefaServico.listarTarefasPorEstudante(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(tarefas)); + } + + @GetMapping("/me/pendentes") + public ResponseEntity>> listarTarefasPendentes( + @AuthenticationPrincipal UserDetails userDetails) { + String estudanteId = userDetails.getUsername(); + List tarefas = tarefaServico.listarTarefasPendentes(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(tarefas)); + } + + @GetMapping("/me/data") + public ResponseEntity>> listarTarefasPorData( + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate data) { + String estudanteId = userDetails.getUsername(); + List tarefas = tarefaServico.listarTarefasPorData(estudanteId, data); + return ResponseEntity.ok(RespostaApi.sucesso(tarefas)); + } + + @GetMapping("/me/periodo") + public ResponseEntity>> listarTarefasPorPeriodo( + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate inicio, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fim) { + String estudanteId = userDetails.getUsername(); + List tarefas = tarefaServico.listarTarefasPorPeriodo(estudanteId, inicio, fim); + return ResponseEntity.ok(RespostaApi.sucesso(tarefas)); + } + + @GetMapping("/{id}") + public ResponseEntity> buscarTarefaPorId(@PathVariable String id) { + RespostaTarefaDTO dto = tarefaServico.buscarTarefaPorId(id); + return ResponseEntity.ok(RespostaApi.sucesso(dto)); + } + + @PutMapping("/{id}") + public ResponseEntity> atualizarTarefa(@PathVariable String id, + @Valid @RequestBody RequisicaoTarefaDTO dto) { + RespostaTarefaDTO tarefa = tarefaServico.atualizarTarefa(id, dto); + return ResponseEntity.ok(RespostaApi.sucesso(tarefa)); + } + + @DeleteMapping("/{id}") + public ResponseEntity> excluirTarefa(@PathVariable String id) { + tarefaServico.excluirTarefa(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null)); + } + + @PatchMapping("/{id}/concluir") + public ResponseEntity> marcarConcluida(@PathVariable String id) { + RespostaTarefaDTO tarefa = tarefaServico.marcarConcluida(id); + return ResponseEntity.ok(RespostaApi.sucesso(tarefa)); + } +} diff --git a/src/main/java/com/agendaestudantil/dto/RequisicaoAtualizacaoEstudanteDTO.java b/src/main/java/com/agendaestudantil/dto/RequisicaoAtualizacaoEstudanteDTO.java new file mode 100644 index 0000000..d5c71b1 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoAtualizacaoEstudanteDTO.java @@ -0,0 +1,12 @@ +package com.agendaestudantil.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; + +public record RequisicaoAtualizacaoEstudanteDTO( + @NotBlank String nome, + @NotBlank String curso, + @NotNull @Min(1) Integer periodo +) { +} diff --git a/src/main/java/com/agendaestudantil/dto/RequisicaoCadastroDTO.java b/src/main/java/com/agendaestudantil/dto/RequisicaoCadastroDTO.java new file mode 100644 index 0000000..cccc5b7 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoCadastroDTO.java @@ -0,0 +1,17 @@ +package com.agendaestudantil.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record RequisicaoCadastroDTO( + @NotBlank String nome, + @Email @NotBlank String email, + @NotBlank @Size(min = 6) String senha, + @NotBlank String curso, + @NotNull @Min(1) Integer periodo, + Boolean consentimentoLgpd +) { +} diff --git a/src/main/java/com/agendaestudantil/dto/RequisicaoDisciplinaDTO.java b/src/main/java/com/agendaestudantil/dto/RequisicaoDisciplinaDTO.java new file mode 100644 index 0000000..a29029e --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoDisciplinaDTO.java @@ -0,0 +1,10 @@ +package com.agendaestudantil.dto; + +import jakarta.validation.constraints.NotBlank; + +public record RequisicaoDisciplinaDTO( + @NotBlank String nome, + String professor, + String sala, + String cor +) {} diff --git a/src/main/java/com/agendaestudantil/dto/RequisicaoEventoDTO.java b/src/main/java/com/agendaestudantil/dto/RequisicaoEventoDTO.java new file mode 100644 index 0000000..314842a --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoEventoDTO.java @@ -0,0 +1,16 @@ +package com.agendaestudantil.dto; + +import com.agendaestudantil.entidade.Evento; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +public record RequisicaoEventoDTO( + @NotBlank String titulo, + String descricao, + @NotNull Evento.TipoEvento tipo, + String local, + String disciplinaId, + @NotNull LocalDateTime dataHora +) {} diff --git a/src/main/java/com/agendaestudantil/dto/RequisicaoLoginDTO.java b/src/main/java/com/agendaestudantil/dto/RequisicaoLoginDTO.java new file mode 100644 index 0000000..fc73032 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoLoginDTO.java @@ -0,0 +1,10 @@ +package com.agendaestudantil.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record RequisicaoLoginDTO( + @Email @NotBlank String email, + @NotBlank String senha +) { +} diff --git a/src/main/java/com/agendaestudantil/dto/RequisicaoTarefaDTO.java b/src/main/java/com/agendaestudantil/dto/RequisicaoTarefaDTO.java new file mode 100644 index 0000000..b0e298a --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoTarefaDTO.java @@ -0,0 +1,18 @@ +package com.agendaestudantil.dto; + +import com.agendaestudantil.entidade.Tarefa; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; + +public record RequisicaoTarefaDTO( + @NotBlank String titulo, + String descricao, + @NotNull(message = "Prioridade é obrigatória") Tarefa.Prioridade prioridade, + Tarefa.StatusTarefa status, + @NotNull @FutureOrPresent LocalDate dataEntrega, + String disciplinaId +) { +} diff --git a/src/main/java/com/agendaestudantil/dto/RequisicaoTrocaSenhaDTO.java b/src/main/java/com/agendaestudantil/dto/RequisicaoTrocaSenhaDTO.java new file mode 100644 index 0000000..a3cd7bf --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoTrocaSenhaDTO.java @@ -0,0 +1,10 @@ +package com.agendaestudantil.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record RequisicaoTrocaSenhaDTO( + @NotBlank String senhaAtual, + @NotBlank @Size(min = 6) String novaSenha +) { +} diff --git a/src/main/java/com/agendaestudantil/dto/RespostaApi.java b/src/main/java/com/agendaestudantil/dto/RespostaApi.java new file mode 100644 index 0000000..69ef1a2 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaApi.java @@ -0,0 +1,9 @@ +package com.agendaestudantil.dto; + +import java.time.LocalDateTime; + +public record RespostaApi(T data, String message, LocalDateTime timestamp) { + public static RespostaApi sucesso(T data) { + return new RespostaApi<>(data, "Sucesso", LocalDateTime.now()); + } +} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/dto/RespostaDadosCompletoDTO.java b/src/main/java/com/agendaestudantil/dto/RespostaDadosCompletoDTO.java new file mode 100644 index 0000000..d2e1f90 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaDadosCompletoDTO.java @@ -0,0 +1,12 @@ +package com.agendaestudantil.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public record RespostaDadosCompletoDTO( + RespostaEstudanteDTO estudante, + List tarefas, + List eventos, + List disciplinas, + LocalDateTime geradoEm +) {} diff --git a/src/main/java/com/agendaestudantil/dto/RespostaDisciplinaDTO.java b/src/main/java/com/agendaestudantil/dto/RespostaDisciplinaDTO.java new file mode 100644 index 0000000..360ebf8 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaDisciplinaDTO.java @@ -0,0 +1,10 @@ +package com.agendaestudantil.dto; + +public record RespostaDisciplinaDTO( + String id, + String estudanteId, + String nome, + String professor, + String sala, + String cor +) {} diff --git a/src/main/java/com/agendaestudantil/dto/RespostaEstudanteDTO.java b/src/main/java/com/agendaestudantil/dto/RespostaEstudanteDTO.java new file mode 100644 index 0000000..264d88a --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaEstudanteDTO.java @@ -0,0 +1,10 @@ +package com.agendaestudantil.dto; + +public record RespostaEstudanteDTO( + String id, + String nome, + String email, + String curso, + Integer periodo +) { +} diff --git a/src/main/java/com/agendaestudantil/dto/RespostaEventoDTO.java b/src/main/java/com/agendaestudantil/dto/RespostaEventoDTO.java new file mode 100644 index 0000000..467d6d0 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaEventoDTO.java @@ -0,0 +1,16 @@ +package com.agendaestudantil.dto; + +import java.time.LocalDateTime; + +public record RespostaEventoDTO( + String id, + String estudanteId, + String titulo, + String descricao, + String tipo, + String local, + String disciplinaId, + LocalDateTime dataHora, + String status, + String nomeDisciplina +) {} diff --git a/src/main/java/com/agendaestudantil/dto/RespostaLoginDTO.java b/src/main/java/com/agendaestudantil/dto/RespostaLoginDTO.java new file mode 100644 index 0000000..3cc2eb2 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaLoginDTO.java @@ -0,0 +1,4 @@ +package com.agendaestudantil.dto; + +public record RespostaLoginDTO(String token, RespostaEstudanteDTO estudante) { +} diff --git a/src/main/java/com/agendaestudantil/dto/RespostaNotificacaoDTO.java b/src/main/java/com/agendaestudantil/dto/RespostaNotificacaoDTO.java new file mode 100644 index 0000000..7dce1f0 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaNotificacaoDTO.java @@ -0,0 +1,14 @@ +package com.agendaestudantil.dto; + +import java.time.LocalDateTime; + +public record RespostaNotificacaoDTO( + String id, + String titulo, + String mensagem, + String tipo, + String referenciaId, + String tipoReferencia, + boolean lida, + LocalDateTime dataGeracao +) {} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/dto/RespostaTarefaDTO.java b/src/main/java/com/agendaestudantil/dto/RespostaTarefaDTO.java new file mode 100644 index 0000000..ae18890 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaTarefaDTO.java @@ -0,0 +1,14 @@ +package com.agendaestudantil.dto; + +import java.time.LocalDate; + +public record RespostaTarefaDTO( + String id, + String titulo, + String descricao, + String prioridade, + String status, + LocalDate dataEntrega, + String disciplinaId, + String estudanteId +) {} diff --git a/src/main/java/com/agendaestudantil/entidade/Disciplina.java b/src/main/java/com/agendaestudantil/entidade/Disciplina.java new file mode 100644 index 0000000..bd24088 --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Disciplina.java @@ -0,0 +1,30 @@ +package com.agendaestudantil.entidade; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Document(collection = "disciplinas") +@CompoundIndex(name = "estudante_nome_idx", def = "{estudanteId: 1, nome: 1}") +public class Disciplina extends EntidadeAuditoria { + + @Id + private String id; + @Indexed + private String estudanteId; + private String nome; + private String professor; + private String sala; + private String cor; +} diff --git a/src/main/java/com/agendaestudantil/entidade/EntidadeAuditoria.java b/src/main/java/com/agendaestudantil/entidade/EntidadeAuditoria.java new file mode 100644 index 0000000..7695feb --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/EntidadeAuditoria.java @@ -0,0 +1,21 @@ +package com.agendaestudantil.entidade; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public abstract class EntidadeAuditoria { + + @CreatedDate + private LocalDateTime dataCriacao; + + @LastModifiedDate + private LocalDateTime dataAtualizacao; +} diff --git a/src/main/java/com/agendaestudantil/entidade/Estudante.java b/src/main/java/com/agendaestudantil/entidade/Estudante.java new file mode 100644 index 0000000..a31af66 --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Estudante.java @@ -0,0 +1,41 @@ +package com.agendaestudantil.entidade; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Document(collection = "estudantes") +public class Estudante extends EntidadeAuditoria { + + @Id + private String id; + + @Indexed(unique = true) + private String email; + + private String nome; + + private String senha; + + private String curso; + + private Integer periodo; + + private Boolean consentimentoLgpd; + + private LocalDateTime dataConsentimento; + + private String versaoTermos; +} diff --git a/src/main/java/com/agendaestudantil/entidade/Evento.java b/src/main/java/com/agendaestudantil/entidade/Evento.java new file mode 100644 index 0000000..9c4ab35 --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Evento.java @@ -0,0 +1,43 @@ +package com.agendaestudantil.entidade; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Document(collection = "eventos") +@CompoundIndex(name = "estudante_data_hora_idx", def = "{estudanteId: 1, dataHora: 1}") +public class Evento extends EntidadeAuditoria { + + @Id + private String id; + @Indexed + private String estudanteId; + private String titulo; + private String descricao; + private TipoEvento tipo; + private String local; + private String disciplinaId; + private LocalDateTime dataHora; + private StatusEvento status; + + public enum TipoEvento { + AULA, PROVA, TRABALHO, ESTUDO, EXAME, OUTRO + } + + public enum StatusEvento { + ATIVO, CANCELADO + } +} diff --git a/src/main/java/com/agendaestudantil/entidade/Notificacao.java b/src/main/java/com/agendaestudantil/entidade/Notificacao.java new file mode 100644 index 0000000..3e34eb6 --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Notificacao.java @@ -0,0 +1,43 @@ +package com.agendaestudantil.entidade; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Document(collection = "notificacoes") +@CompoundIndex(name = "estudante_lida_idx", def = "{estudanteId: 1, lida: 1}") +public class Notificacao extends EntidadeAuditoria { + + @Id + private String id; + @Indexed + private String estudanteId; + private String titulo; + private String mensagem; + private TipoNotificacao tipo; + private String referenciaId; + private TipoReferencia tipoReferencia; + private boolean lida; + private LocalDateTime dataGeracao; + + public enum TipoNotificacao { + PRAZO_PROXIMO, TAREFA_ATRASADA, EVENTO_PROXIMO + } + + public enum TipoReferencia { + TAREFA, EVENTO + } +} diff --git a/src/main/java/com/agendaestudantil/entidade/Tarefa.java b/src/main/java/com/agendaestudantil/entidade/Tarefa.java new file mode 100644 index 0000000..290a1f8 --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Tarefa.java @@ -0,0 +1,46 @@ +package com.agendaestudantil.entidade; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.index.CompoundIndexes; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Document(collection = "tarefas") +@CompoundIndexes({ + @CompoundIndex(name = "estudante_data_entrega_idx", def = "{estudanteId: 1, dataEntrega: 1}"), + @CompoundIndex(name = "estudante_status_idx", def = "{estudanteId: 1, status: 1}") +}) +public class Tarefa extends EntidadeAuditoria { + + @Id + private String id; + private String titulo; + private String descricao; + private Prioridade prioridade; + private StatusTarefa status; + private LocalDate dataEntrega; + private String disciplinaId; + @Indexed + private String estudanteId; + + public enum Prioridade { + BAIXA, MEDIA, ALTA, URGENTE + } + + public enum StatusTarefa { + PENDENTE, EM_ANDAMENTO, CONCLUIDA, ATRASADA + } +} diff --git a/src/main/java/com/agendaestudantil/excecao/ExcecaoNegocio.java b/src/main/java/com/agendaestudantil/excecao/ExcecaoNegocio.java new file mode 100644 index 0000000..d516f1e --- /dev/null +++ b/src/main/java/com/agendaestudantil/excecao/ExcecaoNegocio.java @@ -0,0 +1,7 @@ +package com.agendaestudantil.excecao; + +public class ExcecaoNegocio extends RuntimeException { + public ExcecaoNegocio(String message) { + super(message); + } +} diff --git a/src/main/java/com/agendaestudantil/excecao/ExcecaoRecursoNaoEncontrado.java b/src/main/java/com/agendaestudantil/excecao/ExcecaoRecursoNaoEncontrado.java new file mode 100644 index 0000000..a099dd3 --- /dev/null +++ b/src/main/java/com/agendaestudantil/excecao/ExcecaoRecursoNaoEncontrado.java @@ -0,0 +1,7 @@ +package com.agendaestudantil.excecao; + +public class ExcecaoRecursoNaoEncontrado extends RuntimeException { + public ExcecaoRecursoNaoEncontrado(String message) { + super(message); + } +} diff --git a/src/main/java/com/agendaestudantil/excecao/ManipuladorExcecaoGlobal.java b/src/main/java/com/agendaestudantil/excecao/ManipuladorExcecaoGlobal.java new file mode 100644 index 0000000..1d851f1 --- /dev/null +++ b/src/main/java/com/agendaestudantil/excecao/ManipuladorExcecaoGlobal.java @@ -0,0 +1,62 @@ +package com.agendaestudantil.excecao; + +import com.agendaestudantil.dto.RespostaApi; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class ManipuladorExcecaoGlobal { + + private static final Logger log = LoggerFactory.getLogger(ManipuladorExcecaoGlobal.class); + + @ExceptionHandler(ExcecaoRecursoNaoEncontrado.class) + public ResponseEntity> handleResourceNotFound(ExcecaoRecursoNaoEncontrado ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new RespostaApi<>(null, ex.getMessage(), LocalDateTime.now())); + } + + @ExceptionHandler(ExcecaoNegocio.class) + public ResponseEntity> handleExcecaoNegocio(ExcecaoNegocio ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new RespostaApi<>(null, ex.getMessage(), LocalDateTime.now())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationException( + MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + errors.put(error.getField(), error.getDefaultMessage()); + } + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new RespostaApi<>(errors, "Falha na validação", LocalDateTime.now())); + } + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity> handleResponseStatusException(ResponseStatusException ex) { + String reason = ex.getReason(); + if (reason == null || reason.isBlank()) { + reason = "Acesso não autorizado"; + } + return ResponseEntity.status(ex.getStatusCode()) + .body(new RespostaApi<>(null, reason, LocalDateTime.now())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGenericException(Exception ex) { + log.error("Erro interno no servidor", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new RespostaApi<>(null, "Erro interno no servidor", LocalDateTime.now())); + } +} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/filtro/FiltroJwt.java b/src/main/java/com/agendaestudantil/filtro/FiltroJwt.java new file mode 100644 index 0000000..744901c --- /dev/null +++ b/src/main/java/com/agendaestudantil/filtro/FiltroJwt.java @@ -0,0 +1,62 @@ +package com.agendaestudantil.filtro; + +import com.agendaestudantil.utilitario.UtilJwt; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class FiltroJwt extends OncePerRequestFilter { + + private final UtilJwt utilJwt; + private final UserDetailsService userDetailsService; + + public FiltroJwt(UtilJwt utilJwt, UserDetailsService userDetailsService) { + this.utilJwt = utilJwt; + this.userDetailsService = userDetailsService; + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + return path.equals("/") || path.equals("/index.html") || path.equals("/favicon.ico") + || path.startsWith("/static/") || path.startsWith("/css/") || path.startsWith("/js/") + || path.startsWith("/img/") || path.endsWith(".css") || path.endsWith(".js") + || path.endsWith(".ico") || path.endsWith(".html"); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String header = request.getHeader("Authorization"); + String token = null; + String estudanteId = null; + + if (header != null && header.startsWith("Bearer ")) { + token = header.substring(7); + } + + if (token != null && utilJwt.validateToken(token)) { + estudanteId = utilJwt.getEstudanteIdFromToken(token); + + if (SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(estudanteId); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/repositorio/DisciplinaRepositorio.java b/src/main/java/com/agendaestudantil/repositorio/DisciplinaRepositorio.java new file mode 100644 index 0000000..7433db4 --- /dev/null +++ b/src/main/java/com/agendaestudantil/repositorio/DisciplinaRepositorio.java @@ -0,0 +1,16 @@ +package com.agendaestudantil.repositorio; + +import com.agendaestudantil.entidade.Disciplina; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; +import java.util.Collection; +import java.util.List; + +@Repository +public interface DisciplinaRepositorio extends MongoRepository { + List findByEstudanteId(String estudanteId); + + List findByIdIn(Collection ids); + + void deleteByEstudanteId(String estudanteId); +} diff --git a/src/main/java/com/agendaestudantil/repositorio/EstudanteRepositorio.java b/src/main/java/com/agendaestudantil/repositorio/EstudanteRepositorio.java new file mode 100644 index 0000000..5124576 --- /dev/null +++ b/src/main/java/com/agendaestudantil/repositorio/EstudanteRepositorio.java @@ -0,0 +1,12 @@ +package com.agendaestudantil.repositorio; + +import com.agendaestudantil.entidade.Estudante; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository +public interface EstudanteRepositorio extends MongoRepository { + Optional findByEmail(String email); + boolean existsByEmail(String email); +} diff --git a/src/main/java/com/agendaestudantil/repositorio/EventoRepositorio.java b/src/main/java/com/agendaestudantil/repositorio/EventoRepositorio.java new file mode 100644 index 0000000..187c221 --- /dev/null +++ b/src/main/java/com/agendaestudantil/repositorio/EventoRepositorio.java @@ -0,0 +1,27 @@ +package com.agendaestudantil.repositorio; + +import com.agendaestudantil.entidade.Evento; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + +import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface EventoRepositorio extends MongoRepository { + List findByEstudanteId(String estudanteId); + + List findByDisciplinaId(String disciplinaId); + + @Query("{'estudanteId': ?0, 'dataHora': {$gte: ?1, $lte: ?2}}") + List findByEstudanteIdAndDataHoraBetween(String estudanteId, LocalDateTime inicio, LocalDateTime fim); + + @Query("{'estudanteId': ?0, 'dataHora': {$gte: ?1}}") + List findProximosEventosByEstudanteId(String estudanteId, LocalDateTime data); + + @Query("{'dataHora': {$gte: ?0, $lte: ?1}}") + List findEventosNasProximas24h(LocalDateTime inicio, LocalDateTime fim); + + void deleteByEstudanteId(String estudanteId); +} diff --git a/src/main/java/com/agendaestudantil/repositorio/NotificacaoRepositorio.java b/src/main/java/com/agendaestudantil/repositorio/NotificacaoRepositorio.java new file mode 100644 index 0000000..26a8400 --- /dev/null +++ b/src/main/java/com/agendaestudantil/repositorio/NotificacaoRepositorio.java @@ -0,0 +1,25 @@ +package com.agendaestudantil.repositorio; + +import com.agendaestudantil.entidade.Notificacao; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface NotificacaoRepositorio extends MongoRepository { + + List findByEstudanteIdAndLidaFalse(String estudanteId); + + List findByEstudanteId(String estudanteId); + + long countByEstudanteIdAndLidaFalse(String estudanteId); + + void deleteByEstudanteId(String estudanteId); + + boolean existsByEstudanteIdAndReferenciaId(String estudanteId, String referenciaId); + + boolean existsByEstudanteIdAndReferenciaIdAndTipo(String estudanteId, String referenciaId, Notificacao.TipoNotificacao tipo); + + List findByEstudanteIdAndReferenciaIdAndTipo(String estudanteId, String referenciaId, Notificacao.TipoNotificacao tipo); +} diff --git a/src/main/java/com/agendaestudantil/repositorio/TarefaRepositorio.java b/src/main/java/com/agendaestudantil/repositorio/TarefaRepositorio.java new file mode 100644 index 0000000..70edb7a --- /dev/null +++ b/src/main/java/com/agendaestudantil/repositorio/TarefaRepositorio.java @@ -0,0 +1,32 @@ +package com.agendaestudantil.repositorio; + +import com.agendaestudantil.entidade.Tarefa; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + +import org.springframework.stereotype.Repository; +import java.time.LocalDate; +import java.util.List; + +@Repository +public interface TarefaRepositorio extends MongoRepository { + List findByEstudanteId(String estudanteId); + + List findByEstudanteIdAndStatus(String estudanteId, Tarefa.StatusTarefa status); + + List findByDisciplinaId(String disciplinaId); + + @Query("{'estudanteId': ?0, 'dataEntrega': ?1}") + List findByEstudanteIdAndDataEntrega(String estudanteId, LocalDate data); + + @Query("{'estudanteId': ?0, 'dataEntrega': {$gte: ?1, $lte: ?2}}") + List findByEstudanteIdAndDataEntregaBetween(String estudanteId, LocalDate inicio, LocalDate fim); + + @Query("{'estudanteId': ?0, 'status': {$ne: ?1}}") + List findTarefasPendentesByEstudanteId(String estudanteId, Tarefa.StatusTarefa status); + + @Query("{'status': {$ne: 'CONCLUIDA'}, 'dataEntrega': {$gte: ?0, $lte: ?1}}") + List findTarefasNaoConcluidasComDataEntre(LocalDate inicio, LocalDate fim); + + void deleteByEstudanteId(String estudanteId); +} diff --git a/src/main/java/com/agendaestudantil/seguranca/DetalhesUsuarioPersonalizado.java b/src/main/java/com/agendaestudantil/seguranca/DetalhesUsuarioPersonalizado.java new file mode 100644 index 0000000..d094e09 --- /dev/null +++ b/src/main/java/com/agendaestudantil/seguranca/DetalhesUsuarioPersonalizado.java @@ -0,0 +1,53 @@ +package com.agendaestudantil.seguranca; + +import com.agendaestudantil.entidade.Estudante; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +public class DetalhesUsuarioPersonalizado implements UserDetails { + + private final Estudante estudante; + + public DetalhesUsuarioPersonalizado(Estudante estudante) { + this.estudante = estudante; + } + + @Override + public Collection getAuthorities() { + return Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public String getPassword() { + return estudante.getSenha(); + } + + @Override + public String getUsername() { + return estudante.getId(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/seguranca/ServicoAutenticacaoUsuario.java b/src/main/java/com/agendaestudantil/seguranca/ServicoAutenticacaoUsuario.java new file mode 100644 index 0000000..876f7a0 --- /dev/null +++ b/src/main/java/com/agendaestudantil/seguranca/ServicoAutenticacaoUsuario.java @@ -0,0 +1,26 @@ +package com.agendaestudantil.seguranca; + +import com.agendaestudantil.entidade.Estudante; +import com.agendaestudantil.repositorio.EstudanteRepositorio; +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.Service; + +@Service +public class ServicoAutenticacaoUsuario implements UserDetailsService { + + private final EstudanteRepositorio estudanteRepositorio; + + public ServicoAutenticacaoUsuario(EstudanteRepositorio estudanteRepositorio) { + this.estudanteRepositorio = estudanteRepositorio; + } + + @Override + public UserDetails loadUserByUsername(String estudanteId) throws UsernameNotFoundException { + Estudante estudante = estudanteRepositorio.findById(estudanteId) + .orElseThrow(() -> new UsernameNotFoundException("Estudante não encontrado")); + + return new DetalhesUsuarioPersonalizado(estudante); + } +} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/servico/DisciplinaServico.java b/src/main/java/com/agendaestudantil/servico/DisciplinaServico.java new file mode 100644 index 0000000..f80652e --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/DisciplinaServico.java @@ -0,0 +1,107 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RespostaDisciplinaDTO; +import com.agendaestudantil.entidade.Disciplina; +import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado; +import com.agendaestudantil.repositorio.DisciplinaRepositorio; +import com.agendaestudantil.repositorio.TarefaRepositorio; +import com.agendaestudantil.repositorio.EventoRepositorio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Service +public class DisciplinaServico { + + private static final Logger log = LoggerFactory.getLogger(DisciplinaServico.class); + + private final DisciplinaRepositorio disciplinaRepositorio; + private final TarefaRepositorio tarefaRepositorio; + private final EventoRepositorio eventoRepositorio; + + public DisciplinaServico(DisciplinaRepositorio disciplinaRepositorio, TarefaRepositorio tarefaRepositorio, + EventoRepositorio eventoRepositorio) { + this.disciplinaRepositorio = disciplinaRepositorio; + this.tarefaRepositorio = tarefaRepositorio; + this.eventoRepositorio = eventoRepositorio; + } + + private void validarAcesso(String estudanteId) { + String authUser = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!authUser.equals(estudanteId)) { + log.error("Acesso negado. Usuário {} tentou acessar recurso do estudante {}", authUser, estudanteId); + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Acesso negado"); + } + } + + public RespostaDisciplinaDTO criarDisciplina(Disciplina disciplina, String estudanteId) { + log.debug("Criando disciplina para estudante: {}", estudanteId); + validarAcesso(estudanteId); + + disciplina.setEstudanteId(estudanteId); + Disciplina salva = disciplinaRepositorio.save(disciplina); + return toDTO(salva); + } + + public List listarPorEstudante(String estudanteId) { + log.debug("Listando disciplinas para estudante: {}", estudanteId); + validarAcesso(estudanteId); + + return disciplinaRepositorio.findByEstudanteId(estudanteId).stream() + .map(this::toDTO) + .toList(); + } + + public RespostaDisciplinaDTO buscarPorId(String id, String estudanteId) { + Disciplina disciplina = getDisciplinaEntity(id); + validarAcesso(disciplina.getEstudanteId()); + return toDTO(disciplina); + } + + public RespostaDisciplinaDTO atualizarDisciplina(String id, Disciplina disciplinaAtualizada, String estudanteId) { + Disciplina disciplina = getDisciplinaEntity(id); + validarAcesso(disciplina.getEstudanteId()); + + disciplina.setNome(disciplinaAtualizada.getNome()); + disciplina.setProfessor(disciplinaAtualizada.getProfessor()); + disciplina.setSala(disciplinaAtualizada.getSala()); + disciplina.setCor(disciplinaAtualizada.getCor()); + + return toDTO(disciplinaRepositorio.save(disciplina)); + } + + public void excluirDisciplina(String id, String estudanteId) { + Disciplina disciplina = getDisciplinaEntity(id); + validarAcesso(disciplina.getEstudanteId()); + tarefaRepositorio.findByDisciplinaId(id).forEach(tarefa -> { + tarefa.setDisciplinaId(null); + tarefaRepositorio.save(tarefa); + }); + eventoRepositorio.findByDisciplinaId(id).forEach(evento -> { + evento.setDisciplinaId(null); + eventoRepositorio.save(evento); + }); + disciplinaRepositorio.delete(disciplina); + } + + private Disciplina getDisciplinaEntity(String id) { + return disciplinaRepositorio.findById(id) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Disciplina não encontrada")); + } + + private RespostaDisciplinaDTO toDTO(Disciplina disciplina) { + return new RespostaDisciplinaDTO( + disciplina.getId(), + disciplina.getEstudanteId(), + disciplina.getNome(), + disciplina.getProfessor(), + disciplina.getSala(), + disciplina.getCor() + ); + } +} diff --git a/src/main/java/com/agendaestudantil/servico/EstudanteServico.java b/src/main/java/com/agendaestudantil/servico/EstudanteServico.java new file mode 100644 index 0000000..ee58ad9 --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/EstudanteServico.java @@ -0,0 +1,184 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RequisicaoAtualizacaoEstudanteDTO; +import com.agendaestudantil.dto.RequisicaoCadastroDTO; +import com.agendaestudantil.dto.RespostaDadosCompletoDTO; +import com.agendaestudantil.dto.RespostaEstudanteDTO; +import com.agendaestudantil.dto.RequisicaoLoginDTO; +import com.agendaestudantil.dto.RespostaLoginDTO; +import com.agendaestudantil.dto.RequisicaoTrocaSenhaDTO; +import com.agendaestudantil.dto.RespostaDisciplinaDTO; +import com.agendaestudantil.dto.RespostaEventoDTO; +import com.agendaestudantil.dto.RespostaTarefaDTO; +import com.agendaestudantil.entidade.Estudante; +import com.agendaestudantil.excecao.ExcecaoNegocio; +import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado; +import com.agendaestudantil.repositorio.EstudanteRepositorio; +import com.agendaestudantil.repositorio.EventoRepositorio; +import com.agendaestudantil.repositorio.TarefaRepositorio; +import com.agendaestudantil.repositorio.DisciplinaRepositorio; +import com.agendaestudantil.repositorio.NotificacaoRepositorio; +import com.agendaestudantil.utilitario.UtilJwt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +public class EstudanteServico { + + private static final Logger log = LoggerFactory.getLogger(EstudanteServico.class); + + private final EstudanteRepositorio estudanteRepositorio; + private final PasswordEncoder passwordEncoder; + private final UtilJwt utilJwt; + private final TarefaRepositorio tarefaRepositorio; + private final EventoRepositorio eventoRepositorio; + private final DisciplinaRepositorio disciplinaRepositorio; + private final NotificacaoRepositorio notificacaoRepositorio; + + public EstudanteServico(EstudanteRepositorio estudanteRepositorio, PasswordEncoder passwordEncoder, + UtilJwt utilJwt, TarefaRepositorio tarefaRepositorio, EventoRepositorio eventoRepositorio, + DisciplinaRepositorio disciplinaRepositorio, NotificacaoRepositorio notificacaoRepositorio) { + this.estudanteRepositorio = estudanteRepositorio; + this.passwordEncoder = passwordEncoder; + this.utilJwt = utilJwt; + this.tarefaRepositorio = tarefaRepositorio; + this.eventoRepositorio = eventoRepositorio; + this.disciplinaRepositorio = disciplinaRepositorio; + this.notificacaoRepositorio = notificacaoRepositorio; + } + + public RespostaEstudanteDTO cadastrar(RequisicaoCadastroDTO dto) { + log.debug("Acessando metodo cadastrar para email: {}", dto.email()); + if (!Boolean.TRUE.equals(dto.consentimentoLgpd())) { + throw new ExcecaoNegocio("E necessario aceitar os termos para se cadastrar"); + } + Optional existente = estudanteRepositorio.findByEmail(dto.email()); + if (existente.isPresent()) { + log.error("Email ja cadastrado: {}", dto.email()); + throw new ExcecaoNegocio("Email ja cadastrado"); + } + + Estudante estudante = new Estudante(); + estudante.setNome(dto.nome()); + estudante.setEmail(dto.email()); + estudante.setSenha(passwordEncoder.encode(dto.senha())); + estudante.setCurso(dto.curso()); + estudante.setPeriodo(dto.periodo()); + estudante.setConsentimentoLgpd(dto.consentimentoLgpd()); + estudante.setDataConsentimento(LocalDateTime.now()); + estudante.setVersaoTermos("1.0"); + + Estudante salvo = estudanteRepositorio.save(estudante); + return toResponse(salvo); + } + + public RespostaLoginDTO login(RequisicaoLoginDTO dto) { + log.debug("Acessando metodo login para email: {}", dto.email()); + Optional estudanteParam = estudanteRepositorio.findByEmail(dto.email()); + + if (estudanteParam.isEmpty()) { + log.error("Email ou senha incorretos para email: {}", dto.email()); + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Email ou senha incorretos"); + } + + Estudante estudante = estudanteParam.get(); + + if (!passwordEncoder.matches(dto.senha(), estudante.getSenha())) { + log.error("Falha ao validar senha para email: {}", dto.email()); + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Email ou senha incorretos"); + } + + String token = utilJwt.generateToken(estudante.getId()); + return new RespostaLoginDTO(token, toResponse(estudante)); + } + + public RespostaEstudanteDTO buscarPorId(String id) { + Estudante estudante = estudanteRepositorio.findById(id) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante nao encontrado")); + return toResponse(estudante); + } + + public RespostaEstudanteDTO atualizar(String id, RequisicaoAtualizacaoEstudanteDTO dto) { + Estudante estudante = estudanteRepositorio.findById(id) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante nao encontrado")); + + estudante.setNome(dto.nome()); + estudante.setCurso(dto.curso()); + estudante.setPeriodo(dto.periodo()); + + Estudante atualizado = estudanteRepositorio.save(estudante); + return toResponse(atualizado); + } + + public void trocarSenha(String id, RequisicaoTrocaSenhaDTO dto) { + Estudante estudante = estudanteRepositorio.findById(id) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante nao encontrado")); + + if (!passwordEncoder.matches(dto.senhaAtual(), estudante.getSenha())) { + throw new ExcecaoNegocio("Senha atual incorreta"); + } + + estudante.setSenha(passwordEncoder.encode(dto.novaSenha())); + estudanteRepositorio.save(estudante); + } + + public void excluirConta(String id) { + if (!estudanteRepositorio.existsById(id)) { + throw new ExcecaoRecursoNaoEncontrado("Estudante nao encontrado"); + } + tarefaRepositorio.deleteByEstudanteId(id); + eventoRepositorio.deleteByEstudanteId(id); + disciplinaRepositorio.deleteByEstudanteId(id); + notificacaoRepositorio.deleteByEstudanteId(id); + estudanteRepositorio.deleteById(id); + log.info("Conta e todos os dados do estudante {} removidos (LGPD Art. 18-VI)", id); + } + + public RespostaDadosCompletoDTO exportarDados(String estudanteId) { + RespostaEstudanteDTO estudante = buscarPorId(estudanteId); + + List tarefas = tarefaRepositorio + .findByEstudanteId(estudanteId).stream() + .map(t -> new RespostaTarefaDTO( + t.getId(), t.getTitulo(), t.getDescricao(), + t.getPrioridade() != null ? t.getPrioridade().name() : null, + t.getStatus() != null ? t.getStatus().name() : null, + t.getDataEntrega(), t.getDisciplinaId(), t.getEstudanteId())) + .toList(); + + List eventos = eventoRepositorio + .findByEstudanteId(estudanteId).stream() + .map(e -> new RespostaEventoDTO( + e.getId(), e.getEstudanteId(), e.getTitulo(), e.getDescricao(), + e.getTipo() != null ? e.getTipo().name() : null, + e.getLocal(), e.getDisciplinaId(), e.getDataHora(), + e.getStatus() != null ? e.getStatus().name() : null, + null)) + .toList(); + + List disciplinas = disciplinaRepositorio + .findByEstudanteId(estudanteId).stream() + .map(d -> new RespostaDisciplinaDTO( + d.getId(), d.getEstudanteId(), d.getNome(), d.getProfessor(), d.getSala(), d.getCor())) + .toList(); + + return new RespostaDadosCompletoDTO(estudante, tarefas, eventos, disciplinas, LocalDateTime.now()); + } + + private RespostaEstudanteDTO toResponse(Estudante estudante) { + return new RespostaEstudanteDTO( + estudante.getId(), + estudante.getNome(), + estudante.getEmail(), + estudante.getCurso(), + estudante.getPeriodo()); + } +} diff --git a/src/main/java/com/agendaestudantil/servico/EventoServico.java b/src/main/java/com/agendaestudantil/servico/EventoServico.java new file mode 100644 index 0000000..e148729 --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/EventoServico.java @@ -0,0 +1,170 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RequisicaoEventoDTO; +import com.agendaestudantil.dto.RespostaEventoDTO; +import com.agendaestudantil.entidade.Disciplina; +import com.agendaestudantil.entidade.Evento; +import com.agendaestudantil.excecao.ExcecaoNegocio; +import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado; +import com.agendaestudantil.repositorio.DisciplinaRepositorio; +import com.agendaestudantil.repositorio.EventoRepositorio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class EventoServico { + + private static final Logger log = LoggerFactory.getLogger(EventoServico.class); + + private final EventoRepositorio eventoRepositorio; + private final DisciplinaRepositorio disciplinaRepositorio; + + public EventoServico(EventoRepositorio eventoRepositorio, DisciplinaRepositorio disciplinaRepositorio) { + this.eventoRepositorio = eventoRepositorio; + this.disciplinaRepositorio = disciplinaRepositorio; + } + + private void validarAcesso(String estudanteId) { + String authUser = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!authUser.equals(estudanteId)) { + log.error("Acesso negado. Usuario {} tentou acessar recurso do estudante {}", authUser, estudanteId); + throw new ExcecaoNegocio("Acesso negado"); + } + } + + private void validarDisciplina(String disciplinaId, String estudanteId) { + if (disciplinaId == null || disciplinaId.isBlank()) { + return; + } + Disciplina disciplina = disciplinaRepositorio.findById(disciplinaId) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Disciplina não encontrada")); + if (!disciplina.getEstudanteId().equals(estudanteId)) { + throw new ExcecaoNegocio("Disciplina não pertence ao estudante"); + } + } + + public RespostaEventoDTO criarEvento(Evento evento, String estudanteId) { + log.debug("Criando evento para estudante: {}", estudanteId); + validarAcesso(estudanteId); + + if (evento.getDisciplinaId() != null) { + validarDisciplina(evento.getDisciplinaId(), estudanteId); + } + + evento.setEstudanteId(estudanteId); + Evento salvo = eventoRepositorio.save(evento); + return toDTO(salvo, Map.of()); + } + + public List listarPorEstudante(String estudanteId) { + log.debug("Listando eventos para estudante: {}", estudanteId); + validarAcesso(estudanteId); + + List eventos = eventoRepositorio.findByEstudanteId(estudanteId); + Map disciplinasMap = resolverDisciplinas(eventos); + return eventos.stream() + .map(e -> toDTO(e, disciplinasMap)) + .toList(); + } + + public List listarPorPeriodo(String estudanteId, java.time.LocalDateTime inicio, java.time.LocalDateTime fim) { + validarAcesso(estudanteId); + List eventos = eventoRepositorio.findByEstudanteIdAndDataHoraBetween(estudanteId, inicio, fim); + Map disciplinasMap = resolverDisciplinas(eventos); + return eventos.stream() + .map(e -> toDTO(e, disciplinasMap)) + .toList(); + } + + public List listarProximosEventos(String estudanteId) { + validarAcesso(estudanteId); + List eventos = eventoRepositorio.findProximosEventosByEstudanteId(estudanteId, java.time.LocalDateTime.now()); + Map disciplinasMap = resolverDisciplinas(eventos); + return eventos.stream() + .map(e -> toDTO(e, disciplinasMap)) + .toList(); + } + + public RespostaEventoDTO buscarPorId(String id) { + Evento evento = getEventoEntity(id); + validarAcesso(evento.getEstudanteId()); + return toDTO(evento, Map.of()); + } + + public RespostaEventoDTO atualizarEvento(String id, RequisicaoEventoDTO dto) { + Evento evento = getEventoEntity(id); + validarAcesso(evento.getEstudanteId()); + + evento.setTitulo(dto.titulo()); + evento.setDescricao(dto.descricao()); + evento.setTipo(dto.tipo()); + evento.setLocal(dto.local()); + evento.setDataHora(dto.dataHora()); + + if (dto.disciplinaId() != null) { + validarDisciplina(dto.disciplinaId(), evento.getEstudanteId()); + evento.setDisciplinaId(dto.disciplinaId()); + } + + return toDTO(eventoRepositorio.save(evento), Map.of()); + } + + public void excluirEvento(String id) { + Evento evento = getEventoEntity(id); + validarAcesso(evento.getEstudanteId()); + eventoRepositorio.delete(evento); + } + + public RespostaEventoDTO cancelarEvento(String id) { + Evento evento = getEventoEntity(id); + validarAcesso(evento.getEstudanteId()); + evento.setStatus(Evento.StatusEvento.CANCELADO); + return toDTO(eventoRepositorio.save(evento), Map.of()); + } + + private Evento getEventoEntity(String id) { + return eventoRepositorio.findById(id) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Evento não encontrado")); + } + + private Map resolverDisciplinas(List eventos) { + Set disciplinaIds = eventos.stream() + .map(Evento::getDisciplinaId) + .filter(id -> id != null && !id.isBlank()) + .collect(Collectors.toSet()); + + if (disciplinaIds.isEmpty()) { + return Map.of(); + } + + return disciplinaRepositorio.findByIdIn(disciplinaIds).stream() + .collect(Collectors.toMap(Disciplina::getId, Disciplina::getNome)); + } + + private RespostaEventoDTO toDTO(Evento evento, Map disciplinasMap) { + String nomeDisciplina = null; + final String disciplinaId = evento.getDisciplinaId(); + if (disciplinaId != null) { + nomeDisciplina = disciplinasMap.get(disciplinaId); + } + return new RespostaEventoDTO( + evento.getId(), + evento.getEstudanteId(), + evento.getTitulo(), + evento.getDescricao(), + evento.getTipo() != null ? evento.getTipo().name() : null, + evento.getLocal(), + evento.getDisciplinaId(), + evento.getDataHora(), + evento.getStatus() != null ? evento.getStatus().name() : null, + nomeDisciplina + ); + } +} diff --git a/src/main/java/com/agendaestudantil/servico/NotificacaoAgendadorServico.java b/src/main/java/com/agendaestudantil/servico/NotificacaoAgendadorServico.java new file mode 100644 index 0000000..1bc664a --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/NotificacaoAgendadorServico.java @@ -0,0 +1,132 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.entidade.Evento; +import com.agendaestudantil.entidade.Notificacao; +import com.agendaestudantil.entidade.Tarefa; +import com.agendaestudantil.repositorio.EventoRepositorio; +import com.agendaestudantil.repositorio.NotificacaoRepositorio; +import com.agendaestudantil.repositorio.TarefaRepositorio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Service +public class NotificacaoAgendadorServico { + + private static final Logger log = LoggerFactory.getLogger(NotificacaoAgendadorServico.class); + + private final TarefaRepositorio tarefaRepositorio; + private final EventoRepositorio eventoRepositorio; + private final NotificacaoRepositorio notificacaoRepositorio; + + public NotificacaoAgendadorServico(TarefaRepositorio tarefaRepositorio, EventoRepositorio eventoRepositorio, + NotificacaoRepositorio notificacaoRepositorio) { + this.tarefaRepositorio = tarefaRepositorio; + this.eventoRepositorio = eventoRepositorio; + this.notificacaoRepositorio = notificacaoRepositorio; + } + + @Scheduled(fixedRate = 3600000) + public void gerarNotificacoes() { + log.debug("Iniciando geracao de notificacoes"); + LocalDate hoje = LocalDate.now(); + LocalDate ontem = hoje.minusDays(1); + LocalDate tresDiasDepois = hoje.plusDays(3); + + List tarefas = tarefaRepositorio.findTarefasNaoConcluidasComDataEntre(ontem, tresDiasDepois); + for (Tarefa tarefa : tarefas) { + if (tarefa.getStatus() == Tarefa.StatusTarefa.CONCLUIDA) { + continue; + } + + LocalDate dataEntrega = tarefa.getDataEntrega(); + if (dataEntrega == null) { + continue; + } + + long diasAteEntrega = ChronoUnit.DAYS.between(hoje, dataEntrega); + + if (dataEntrega.isBefore(hoje)) { + if (!tarefa.getStatus().equals(Tarefa.StatusTarefa.ATRASADA)) { + tarefa.setStatus(Tarefa.StatusTarefa.ATRASADA); + tarefaRepositorio.save(tarefa); + } + + if (!notificacaoRepositorio.existsByEstudanteIdAndReferenciaIdAndTipo( + tarefa.getEstudanteId(), tarefa.getId(), Notificacao.TipoNotificacao.TAREFA_ATRASADA)) { + criarNotificacao(tarefa.getEstudanteId(), tarefa.getId(), + "Tarefa atrasada: " + tarefa.getTitulo(), + "A tarefa '" + tarefa.getTitulo() + "' esta atrasada desde " + dataEntrega, + Notificacao.TipoNotificacao.TAREFA_ATRASADA, + Notificacao.TipoReferencia.TAREFA); + } + } else if (diasAteEntrega == 0 || diasAteEntrega == 1 || diasAteEntrega == 2 || diasAteEntrega == 3) { + boolean enviarNotificacao = false; + if (diasAteEntrega == 0 || diasAteEntrega == 1) { + enviarNotificacao = true; + } else if (diasAteEntrega == 2 || diasAteEntrega == 3) { + if (tarefa.getPrioridade() == Tarefa.Prioridade.ALTA || tarefa.getPrioridade() == Tarefa.Prioridade.URGENTE) { + enviarNotificacao = true; + } + } + + if (enviarNotificacao) { + boolean jaNotificouHoje = notificacaoRepositorio.findByEstudanteIdAndReferenciaIdAndTipo( + tarefa.getEstudanteId(), tarefa.getId(), Notificacao.TipoNotificacao.PRAZO_PROXIMO) + .stream() + .anyMatch(n -> n.getDataGeracao().toLocalDate().equals(hoje)); + + if (!jaNotificouHoje) { + String prazo = diasAteEntrega == 0 ? "hoje" : (diasAteEntrega == 1 ? "amanha" : "em " + diasAteEntrega + " dias"); + criarNotificacao(tarefa.getEstudanteId(), tarefa.getId(), + "Tarefa com prazo proximo: " + tarefa.getTitulo(), + "A tarefa '" + tarefa.getTitulo() + "' vence " + prazo, + Notificacao.TipoNotificacao.PRAZO_PROXIMO, + Notificacao.TipoReferencia.TAREFA); + } + } + } + } + + LocalDateTime agora = LocalDateTime.now(); + LocalDateTime em24Horas = agora.plusHours(24); + List eventos = eventoRepositorio.findEventosNasProximas24h(agora, em24Horas); + for (Evento evento : eventos) { + if (evento.getDataHora() == null) { + continue; + } + + if (!notificacaoRepositorio.existsByEstudanteIdAndReferenciaIdAndTipo( + evento.getEstudanteId(), evento.getId(), Notificacao.TipoNotificacao.EVENTO_PROXIMO)) { + criarNotificacao(evento.getEstudanteId(), evento.getId(), + "Evento nas proximas 24h: " + evento.getTitulo(), + "O evento '" + evento.getTitulo() + "' sera em " + evento.getDataHora(), + Notificacao.TipoNotificacao.EVENTO_PROXIMO, + Notificacao.TipoReferencia.EVENTO); + } + } + + log.debug("Geracao de notificacoes concluida"); + } + + private void criarNotificacao(String estudanteId, String referenciaId, String titulo, String mensagem, + Notificacao.TipoNotificacao tipo, Notificacao.TipoReferencia tipoReferencia) { + Notificacao notif = new Notificacao(); + notif.setEstudanteId(estudanteId); + notif.setReferenciaId(referenciaId); + notif.setTitulo(titulo); + notif.setMensagem(mensagem); + notif.setTipo(tipo); + notif.setTipoReferencia(tipoReferencia); + notif.setLida(false); + notif.setDataGeracao(LocalDateTime.now()); + notificacaoRepositorio.save(notif); + log.debug("Notificacao criada para estudante {}", estudanteId); + } +} diff --git a/src/main/java/com/agendaestudantil/servico/NotificacaoServico.java b/src/main/java/com/agendaestudantil/servico/NotificacaoServico.java new file mode 100644 index 0000000..6cf6c6b --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/NotificacaoServico.java @@ -0,0 +1,98 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RespostaNotificacaoDTO; +import com.agendaestudantil.entidade.Notificacao; +import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado; +import com.agendaestudantil.repositorio.NotificacaoRepositorio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class NotificacaoServico { + + private static final Logger log = LoggerFactory.getLogger(NotificacaoServico.class); + + private final NotificacaoRepositorio notificacaoRepositorio; + + public NotificacaoServico(NotificacaoRepositorio notificacaoRepositorio) { + this.notificacaoRepositorio = notificacaoRepositorio; + } + + private void validarAcesso(String estudanteId) { + String authUser = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!authUser.equals(estudanteId)) { + log.error("Acesso negado. Usuário {} tentou acessar recurso do estudante {}", authUser, estudanteId); + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Acesso negado"); + } + } + + public List listarNaoLidas(String estudanteId) { + log.debug("Listando notificações não lidas para estudante: {}", estudanteId); + validarAcesso(estudanteId); + return notificacaoRepositorio.findByEstudanteIdAndLidaFalse(estudanteId).stream() + .map(this::toDTO) + .toList(); + } + + public List listarTodas(String estudanteId) { + log.debug("Listando todas as notificações para estudante: {}", estudanteId); + validarAcesso(estudanteId); + return notificacaoRepositorio.findByEstudanteId(estudanteId).stream() + .map(this::toDTO) + .toList(); + } + + public long contarNaoLidas(String estudanteId) { + log.debug("Contando notificações não lidas para estudante: {}", estudanteId); + validarAcesso(estudanteId); + return notificacaoRepositorio.countByEstudanteIdAndLidaFalse(estudanteId); + } + + public RespostaNotificacaoDTO marcarComoLida(String id) { + Notificacao notificacao = getNotificacaoEntity(id); + validarAcesso(notificacao.getEstudanteId()); + notificacao.setLida(true); + return toDTO(notificacaoRepositorio.save(notificacao)); + } + + public void marcarTodasComoLidas(String estudanteId) { + log.debug("Marcando todas as notificações como lidas para estudante: {}", estudanteId); + validarAcesso(estudanteId); + List notificacoes = notificacaoRepositorio.findByEstudanteIdAndLidaFalse(estudanteId); + for (Notificacao n : notificacoes) { + n.setLida(true); + notificacaoRepositorio.save(n); + } + } + + public void excluirNotificacao(String id) { + Notificacao notificacao = getNotificacaoEntity(id); + validarAcesso(notificacao.getEstudanteId()); + notificacaoRepositorio.delete(notificacao); + } + + private Notificacao getNotificacaoEntity(String id) { + return notificacaoRepositorio.findById(id) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Notificação não encontrada")); + } + + private RespostaNotificacaoDTO toDTO(Notificacao notificacao) { + return new RespostaNotificacaoDTO( + notificacao.getId(), + notificacao.getTitulo(), + notificacao.getMensagem(), + notificacao.getTipo() != null ? notificacao.getTipo().name() : null, + notificacao.getReferenciaId(), + notificacao.getTipoReferencia() != null ? notificacao.getTipoReferencia().name() : null, + notificacao.isLida(), + notificacao.getDataGeracao() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/agendaestudantil/servico/TarefaServico.java b/src/main/java/com/agendaestudantil/servico/TarefaServico.java new file mode 100644 index 0000000..295ed11 --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/TarefaServico.java @@ -0,0 +1,143 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RequisicaoTarefaDTO; +import com.agendaestudantil.dto.RespostaTarefaDTO; +import com.agendaestudantil.entidade.Tarefa; +import com.agendaestudantil.excecao.ExcecaoNegocio; +import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado; +import com.agendaestudantil.repositorio.TarefaRepositorio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class TarefaServico { + + private static final Logger log = LoggerFactory.getLogger(TarefaServico.class); + + private final TarefaRepositorio tarefaRepositorio; + + public TarefaServico(TarefaRepositorio tarefaRepositorio) { + this.tarefaRepositorio = tarefaRepositorio; + } + + private void validarAcesso(String estudanteId) { + String authUser = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!authUser.equals(estudanteId)) { + log.error("Acesso negado. Usuario {} tentou acessar recurso do estudante {}", authUser, estudanteId); + throw new ExcecaoNegocio("Acesso negado"); + } + } + + private void validarAcessoTarefa(Tarefa tarefa) { + validarAcesso(tarefa.getEstudanteId()); + } + + public RespostaTarefaDTO criarTarefa(RequisicaoTarefaDTO dto) { + String estudanteId = SecurityContextHolder.getContext().getAuthentication().getName(); + log.debug("Criando tarefa para estudante: {}", estudanteId); + + Tarefa tarefa = new Tarefa(); + tarefa.setTitulo(dto.titulo()); + tarefa.setDescricao(dto.descricao()); + tarefa.setPrioridade(dto.prioridade() != null ? dto.prioridade() : Tarefa.Prioridade.MEDIA); + tarefa.setStatus(dto.status() != null ? dto.status() : Tarefa.StatusTarefa.PENDENTE); + tarefa.setDataEntrega(dto.dataEntrega()); + tarefa.setEstudanteId(estudanteId); + + if (dto.disciplinaId() != null) { + tarefa.setDisciplinaId(dto.disciplinaId()); + } + + return toDTO(tarefaRepositorio.save(tarefa)); + } + + public List listarTarefasPorEstudante(String estudanteId) { + log.debug("Listando tarefas para estudante: {}", estudanteId); + validarAcesso(estudanteId); + return tarefaRepositorio.findByEstudanteId(estudanteId).stream() + .map(this::toDTO) + .toList(); + } + + public List listarTarefasPendentes(String estudanteId) { + log.debug("Listando tarefas pendentes para estudante: {}", estudanteId); + validarAcesso(estudanteId); + return tarefaRepositorio.findByEstudanteIdAndStatus(estudanteId, Tarefa.StatusTarefa.PENDENTE).stream() + .map(this::toDTO) + .toList(); + } + + public List listarTarefasPorData(String estudanteId, LocalDate data) { + validarAcesso(estudanteId); + return tarefaRepositorio.findByEstudanteIdAndDataEntrega(estudanteId, data).stream() + .map(this::toDTO) + .toList(); + } + + public List listarTarefasPorPeriodo(String estudanteId, LocalDate inicio, LocalDate fim) { + validarAcesso(estudanteId); + return tarefaRepositorio.findByEstudanteIdAndDataEntregaBetween(estudanteId, inicio, fim).stream() + .map(this::toDTO) + .toList(); + } + + public RespostaTarefaDTO buscarTarefaPorId(String id) { + return toDTO(getTarefaEntity(id)); + } + + public RespostaTarefaDTO atualizarTarefa(String id, RequisicaoTarefaDTO dto) { + Tarefa tarefa = getTarefaEntity(id); + String authUser = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!authUser.equals(tarefa.getEstudanteId())) { + throw new ExcecaoNegocio("Acesso negado"); + } + + tarefa.setTitulo(dto.titulo()); + tarefa.setDescricao(dto.descricao()); + tarefa.setPrioridade(dto.prioridade() != null ? dto.prioridade() : tarefa.getPrioridade()); + tarefa.setStatus(dto.status() != null ? dto.status() : tarefa.getStatus()); + if (dto.dataEntrega() != null) { + tarefa.setDataEntrega(dto.dataEntrega()); + } + + if (dto.disciplinaId() != null) { + tarefa.setDisciplinaId(dto.disciplinaId()); + } + + return toDTO(tarefaRepositorio.save(tarefa)); + } + + public void excluirTarefa(String id) { + tarefaRepositorio.delete(getTarefaEntity(id)); + } + + public RespostaTarefaDTO marcarConcluida(String id) { + Tarefa tarefa = getTarefaEntity(id); + tarefa.setStatus(Tarefa.StatusTarefa.CONCLUIDA); + return toDTO(tarefaRepositorio.save(tarefa)); + } + + private Tarefa getTarefaEntity(String id) { + Tarefa tarefa = tarefaRepositorio.findById(id) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Tarefa não encontrada")); + validarAcessoTarefa(tarefa); + return tarefa; + } + + private RespostaTarefaDTO toDTO(Tarefa tarefa) { + return new RespostaTarefaDTO( + tarefa.getId(), + tarefa.getTitulo(), + tarefa.getDescricao(), + tarefa.getPrioridade() != null ? tarefa.getPrioridade().name() : null, + tarefa.getStatus() != null ? tarefa.getStatus().name() : null, + tarefa.getDataEntrega(), + tarefa.getDisciplinaId(), + tarefa.getEstudanteId()); + } +} diff --git a/src/main/java/com/agendaestudantil/utilitario/UtilJwt.java b/src/main/java/com/agendaestudantil/utilitario/UtilJwt.java new file mode 100644 index 0000000..ec15a21 --- /dev/null +++ b/src/main/java/com/agendaestudantil/utilitario/UtilJwt.java @@ -0,0 +1,58 @@ +package com.agendaestudantil.utilitario; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +public class UtilJwt { + + private final String secret; + private final long jwtExpiration; + + public UtilJwt(@Value("${jwt.secret}") String secret, @Value("${jwt.expiration}") long jwtExpiration) { + this.secret = secret; + this.jwtExpiration = jwtExpiration; + } + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String estudanteId) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtExpiration); + + return Jwts.builder() + .subject(estudanteId) + .issuedAt(now) + .expiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + public String getEstudanteIdFromToken(String token) { + Claims claims = Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + + return claims.getSubject(); + } + + public boolean validateToken(String token) { + try { + Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token); + return true; + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties new file mode 100644 index 0000000..a8cdcf9 --- /dev/null +++ b/src/main/resources/application-dev.properties @@ -0,0 +1,7 @@ +spring.data.mongodb.uri=${MONGO_URI:mongodb://localhost:27017/agenda_estudantil} +cors.allowed.origins=${CORS_ORIGINS:http://localhost:8080,http://localhost:3000} +jwt.secret=${JWT_SECRET:8a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7A8B9C0D1E2F3} +jwt.expiration=${JWT_EXPIRATION:86400000} +logging.level.org.springframework.data.mongodb=DEBUG +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..9d9daab --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,8 @@ +spring.data.mongodb.uri=${MONGO_URI} +cors.allowed.origins=${CORS_ORIGINS} +jwt.secret=${JWT_SECRET} +springdoc.api-docs.enabled=false +springdoc.swagger-ui.enabled=false +logging.level.root=WARN +logging.level.root=WARN +logging.level.com.agendaestudantil=INFO diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..4be5757 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,13 @@ +spring.application.name=${APP_NAME:Focus Agenda} +server.port=${SERVER_PORT:8080} +server.servlet.context-path=/ +spring.web.resources.static-locations=classpath:/static/ +spring.mvc.static-path-pattern=/** +spring.mvc.contentnegotiation.favor-parameter=true +spring.web.resources.add-mappings=true +spring.servlet.multipart.enabled=false + +spring.web.resources.cache.cachecontrol.max-age=3600 +spring.web.resources.cache.cachecontrol.cache-public=true + +spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev} diff --git a/src/main/resources/static/cadastro.css b/src/main/resources/static/cadastro.css new file mode 100644 index 0000000..cbc62b7 --- /dev/null +++ b/src/main/resources/static/cadastro.css @@ -0,0 +1,182 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + min-height: 100vh; + font-family: 'Poppins', Arial, sans-serif; + display: flex; + align-items: center; + justify-content: center; + padding: 80px 20px 20px; + background: #f5f5f5; +} + +#topo { + width: 100%; + height: 50px; + position: fixed; + top: 0; left: 0; + background: linear-gradient(to right, #c0392b 47%, #7a4951 73%, #114455 87%); + display: flex; + align-items: center; + z-index: 10; +} + +#textotop { + padding-left: 20px; + font-size: clamp(22px, 5vw, 38px); + color: #fff; + font-weight: 500; +} + +#log { + width: 100%; + max-width: 400px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.mens { text-align: center; color: #1f2937; } +.campo { display: flex; flex-direction: column; gap: 8px; } +label { font-weight: 700; color: #1f2937; } + +input, select { + height: 46px; + width: 100%; + padding: 10px; + font-size: 16px; + border: 1px solid #c7c7c7; + border-radius: 6px; + font-family: inherit; + background: #fff; +} + +form { display: flex; flex-direction: column; gap: 16px; } + +.linha-dupla { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + +#logbtn { + align-self: center; + width: 50%; + padding: 12px; + font-size: 18px; + font-weight: bold; + background-color: #c0392b; + color: #fff; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +#logbtn:hover { background-color: #a03224; } +#logbtn:disabled { background-color: #ccc; cursor: not-allowed; } + +a { color: #111; text-decoration: none; } +a:hover { text-decoration: underline; } + +#mensagem-erro { + background: #fee2e2; + border: 1px solid #fca5a5; + color: #b91c1c; + padding: 10px 14px; + border-radius: 6px; + font-size: 14px; + display: none; +} + +#mensagem-sucesso { + background: #d1fae5; + border: 1px solid #6ee7b7; + color: #065f46; + padding: 10px 14px; + border-radius: 6px; + font-size: 14px; + display: none; +} + +.campo-consentimento { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 12px 14px; +} + +.label-checkbox { + display: flex; + align-items: flex-start; + gap: 10px; + cursor: pointer; + font-size: 13px; + color: #374151; + font-weight: normal; +} + +.label-checkbox input[type="checkbox"] { + flex-shrink: 0; + width: 16px; + height: 16px; + margin-top: 2px; + accent-color: #c0392b; +} + +.label-checkbox a { + color: #c0392b; + text-decoration: underline; +} + +.theme-toggle-btn { + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + background: rgba(255,255,255,0.15); + border: 1px solid rgba(255,255,255,0.3); + width: 36px; + height: 36px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s, transform 0.2s; + padding: 4px; +} +.theme-toggle-btn:hover { background: rgba(255,255,255,0.3); transform: translateY(-50%) scale(1.1); } +.theme-icon { width: 100%; height: 100%; object-fit: contain; } + +[data-theme="dark"] body { + background: #121212; + color: #e8e8e8; +} + +[data-theme="dark"] .mens { color: #e8e8e8; } +[data-theme="dark"] label { color: #e8e8e8; } + +[data-theme="dark"] input, +[data-theme="dark"] select { + background: #1a1a1a; + border-color: #333; + color: #e8e8e8; +} + +[data-theme="dark"] #mensagem-erro { + background: rgba(254,226,226,0.1); + border-color: rgba(252,165,165,0.2); + color: #fca5a5; +} + +[data-theme="dark"] #mensagem-sucesso { + background: rgba(209,250,229,0.1); + border-color: rgba(110,231,183,0.2); + color: #6ee7b7; +} + +[data-theme="dark"] a { color: #e8e8e8; } + +[data-theme="dark"] .campo-consentimento { + background: #1a1a1a; + border-color: #333; +} + +[data-theme="dark"] .label-checkbox { color: #e8e8e8; } diff --git a/src/main/resources/static/cadastro.html b/src/main/resources/static/cadastro.html new file mode 100644 index 0000000..e2b142c --- /dev/null +++ b/src/main/resources/static/cadastro.html @@ -0,0 +1,174 @@ + + + + + + + + + Cadastro - Focus Agenda + + + +
+

Focus Agenda

+ +
+ +
+

Crie Sua Conta

+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +

Ja tem uma conta?

+
+ + + + diff --git a/src/main/resources/static/calendario.css b/src/main/resources/static/calendario.css new file mode 100644 index 0000000..6d6d744 --- /dev/null +++ b/src/main/resources/static/calendario.css @@ -0,0 +1,519 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: 'Poppins', sans-serif; + background: #f5f5f5; +} + +#header { + width: 100%; + height: 50px; + position: fixed; + top: 0; left: 0; + background: linear-gradient(to right, #c0392b, #114455); + display: flex; + align-items: center; + z-index: 10; +} + +#title { color: #fff; padding-left: 20px; font-size: 28px; } + +#barraesquerda { + position: fixed; + top: 50px; left: 0; + width: 280px; + height: calc(100vh - 50px); + background: #c0392b; + padding: 15px; + color: #fff; + display: flex; + flex-direction: column; + font-family: 'Inter', sans-serif; + overflow-y: auto; + z-index: 5; +} + +#calendario { margin-top: 10px; } +.calendariotop { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } +#mes { font-size: 16px; font-weight: 600; } +#calendarseta { display: flex; gap: 10px; } +#calendarseta button { background: transparent; border: none; color: #fff; font-size: 24px; cursor: pointer; padding: 2px 8px; transition: transform 0.2s; } +#calendarseta button:hover { transform: scale(1.2); } + +.calendariodia { width: 100%; border-collapse: collapse; } +.calendariodia th { font-size: 10px; opacity: 0.8; padding: 3px; text-align: center; } +.calendariodia td { text-align: center; padding: 4px; font-size: 11px; border-radius: 50%; cursor: pointer; transition: background 0.15s; } +.calendariodia td:hover { background: rgba(255,255,255,0.2); } +.calendariodia td.outromes { opacity: 0.4; } +.calendariodia td.today { background: #fff; color: #c0392b; font-weight: 700; border-radius: 50%; } +.calendariodia td.selecionado { background: rgba(255,255,255,0.35); border-radius: 50%; } + +#agenda { margin-top: 18px; } +.agenda-header { font-size: 12px; font-weight: 700; opacity: 0.8; margin-bottom: 8px; letter-spacing: 0.5px; } +.agenda-empty { font-size: 12px; opacity: 0.7; font-style: italic; } +.evento { background: rgba(255,255,255,0.15); border-radius: 8px; padding: 8px 10px; margin-bottom: 8px; cursor: pointer; transition: background 0.15s; } +.evento:hover { background: rgba(255,255,255,0.25); } +.evento .hora { font-size: 11px; opacity: 0.8; } +.evento .titulo { font-size: 13px; font-weight: 600; } + +#feriados { margin-top: 16px; } +.feriados-header { font-size: 12px; font-weight: 700; opacity: 0.8; margin-bottom: 8px; letter-spacing: 0.5px; } +.feriado { display: flex; align-items: center; gap: 8px; font-size: 12px; margin-bottom: 4px; } +.dot { width: 8px; height: 8px; background: #fff; border-radius: 50%; flex-shrink: 0; } + +.main { + margin-left: 280px; + margin-top: 50px; + padding: 24px; + min-height: calc(100vh - 50px); +} + +.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } +.topbar h1 { font-size: 24px; color: #1f2937; } + +.user-area { display: flex; align-items: center; gap: 14px; } +.icone-img { width: 28px; height: 28px; cursor: pointer; opacity: 0.7; transition: opacity 0.2s; } +.icone-img:hover { opacity: 1; } + +.perfil { display: flex; align-items: center; gap: 10px; } +.avatar { width: 38px; height: 38px; border-radius: 50%; background: linear-gradient(135deg, #c0392b, #114455); } +.info { display: flex; flex-direction: column; } +.nome { font-size: 14px; font-weight: 600; color: #1f2937; } +.cargo { font-size: 12px; color: #6b7280; } + +#btnLogout { + background: transparent; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 6px 12px; + font-size: 13px; + cursor: pointer; + color: #6b7280; + transition: all 0.2s; +} +#btnLogout:hover { background: #fee2e2; border-color: #c0392b; color: #c0392b; } + +#btnConfig { + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.6; + transition: opacity 0.2s, transform 0.2s; + cursor: pointer; +} +#btnConfig:hover { opacity: 1; transform: scale(1.15); } +.config-icon { width: 22px; height: 22px; object-fit: contain; } + +.theme-toggle-btn { + background: transparent; + border: 1px solid #d1d5db; + border-radius: 6px; + width: 36px; + height: 36px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + padding: 4px; +} +.theme-toggle-btn:hover { background: #f3f4f6; transform: scale(1.1); } +.theme-icon { width: 100%; height: 100%; object-fit: contain; } + +.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 10px; } + +.mes-nav { display: flex; align-items: center; gap: 12px; } +.seta { background: transparent; border: 1px solid #d1d5db; border-radius: 6px; width: 32px; height: 32px; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.15s; } +.seta:hover { background: #f3f4f6; } +.titulo-mes { font-size: 18px; font-weight: 600; color: #1f2937; min-width: 200px; text-align: center; } + +.view-switch { display: flex; gap: 4px; background: #f3f4f6; padding: 4px; border-radius: 8px; } +.view-switch button { border: none; background: transparent; padding: 6px 14px; border-radius: 6px; font-size: 14px; cursor: pointer; color: #6b7280; transition: all 0.2s; font-family: inherit; } +.view-switch button.active { background: #fff; color: #1f2937; font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.12); } + +#btnNovoEvento { + background: #c0392b; + color: #fff; + border: none; + border-radius: 8px; + padding: 8px 18px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + font-family: inherit; +} +#btnNovoEvento:hover { background: #a03224; } +#btnGerenciarDisciplinas { + background: #c0392b; + color: #fff; + border: none; + border-radius: 8px; + padding: 8px 18px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + font-family: inherit; +} +#btnGerenciarDisciplinas:hover { background: #a03224; } + +.calendar-area { background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); overflow: hidden; } + +.month-view { display: grid; grid-template-columns: repeat(7, 1fr); } +.dia-semana { text-align: center; padding: 10px; font-size: 12px; font-weight: 600; color: #6b7280; background: #f9fafb; border-bottom: 1px solid #e5e7eb; } +.dia-box { min-height: 100px; padding: 8px; border-right: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: background 0.1s; } +.dia-box:hover { background: #fef2f2; } +.dia-box:nth-child(7n) { border-right: none; } +.dia-box.outro-mes { background: #fafafa; color: #d1d5db; } +.dia-box.outro-mes .num-dia { color: #d1d5db; } +.num-dia { font-size: 13px; font-weight: 500; color: #374151; margin-bottom: 4px; } +.dia-box.today .num-dia { background: #c0392b; color: #fff; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; } +.dia-box.selecionado { background: #fef2f2; } +.evento-mini { font-size: 10px; background: #c0392b; color: #fff; border-radius: 3px; padding: 1px 4px; margin-top: 2px; truncate: clip; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.evento-mini.verde { background: #16a34a; } +.evento-mini.azul { background: #1d4ed8; } +.evento-mini.amarelo { background: #d97706; } +.mais-eventos { font-size: 10px; color: #6b7280; margin-top: 2px; cursor: pointer; } + +.week-view { display: grid; grid-template-columns: repeat(7, 1fr); } +.week-col { border-right: 1px solid #e5e7eb; padding: 10px 8px; min-height: 300px; } +.week-col:last-child { border-right: none; } +.week-col.today { background: #fef9f9; } +.week-col-head { font-size: 13px; font-weight: 600; color: #6b7280; margin-bottom: 6px; } +.week-col-date { display: block; font-size: 11px; font-weight: 400; color: #9ca3af; } +.week-events { display: flex; flex-direction: column; gap: 6px; } +.week-empty { font-size: 11px; color: #d1d5db; font-style: italic; } + +.day-panel { padding: 20px; } +.day-panel-header { font-size: 16px; font-weight: 600; color: #374151; margin-bottom: 16px; } +.day-events { display: flex; flex-direction: column; gap: 10px; } +.day-empty { color: #9ca3af; font-style: italic; font-size: 14px; } + +.calendar-event { border-radius: 6px; padding: 8px 10px; background: #fee2e2; border-left: 4px solid #c0392b; } +.calendar-event.verde { background: #d1fae5; border-color: #16a34a; } +.calendar-event.azul { background: #dbeafe; border-color: #1d4ed8; } +.calendar-event.amarelo { background: #fef3c7; border-color: #d97706; } +.calendar-event.rosa { background: #fce7f3; border-color: #db2777; } +.calendar-event-hora { font-size: 11px; color: #6b7280; } +.calendar-event-titulo { font-size: 13px; font-weight: 600; color: #1f2937; } + +.loading { display: flex; align-items: center; justify-content: center; padding: 40px; color: #9ca3af; gap: 10px; font-size: 14px; } +.spinner { width: 20px; height: 20px; border: 2px solid #e5e7eb; border-top-color: #c0392b; border-radius: 50%; animation: spin 0.7s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +.modal-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + z-index: 100; + align-items: center; + justify-content: center; +} +.modal-overlay.aberto { display: flex; } + +.modal { + background: #fff; + border-radius: 12px; + padding: 28px; + width: 100%; + max-width: 480px; + margin: 20px; + box-shadow: 0 20px 60px rgba(0,0,0,0.2); + max-height: 90vh; + overflow-y: auto; +} + +.modal-titulo { font-size: 18px; font-weight: 700; color: #1f2937; margin-bottom: 20px; } + +.modal-campo { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; } +.modal-campo label { font-size: 13px; font-weight: 600; color: #374151; } +.modal-campo input, +.modal-campo select, +.modal-campo textarea { + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 10px 12px; + font-size: 14px; + font-family: inherit; + transition: border-color 0.2s; + background: #fff; +} +.modal-campo input:focus, +.modal-campo select:focus, +.modal-campo textarea:focus { + outline: none; + border-color: #c0392b; +} +.modal-campo textarea { resize: vertical; min-height: 70px; } + +.modal-linha { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + +.modal-acoes { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } + +.btn-secundario { + background: transparent; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 9px 18px; + font-size: 14px; + cursor: pointer; + color: #6b7280; + font-family: inherit; + transition: all 0.2s; +} +.btn-secundario:hover { background: #f3f4f6; } + +.btn-primario { + background: #c0392b; + color: #fff; + border: none; + border-radius: 8px; + padding: 9px 18px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + transition: background 0.2s; +} +.btn-primario:hover { background: #a03224; } +.btn-primario:disabled { background: #ccc; cursor: not-allowed; } + +.btn-perigo { + background: transparent; + border: 1px solid #fca5a5; + color: #c0392b; + border-radius: 8px; + padding: 9px 18px; + font-size: 14px; + cursor: pointer; + font-family: inherit; + transition: all 0.2s; + margin-right: auto; +} +.btn-perigo:hover { background: #fee2e2; } + +#toast { + position: fixed; + bottom: 24px; + right: 24px; + background: #1f2937; + color: #fff; + padding: 12px 18px; + border-radius: 8px; + font-size: 14px; + z-index: 200; + transform: translateY(80px); + opacity: 0; + transition: all 0.3s; + max-width: 320px; +} +#toast.visivel { transform: translateY(0); opacity: 1; } +#toast.sucesso { background: #065f46; } +#toast.erro { background: #b91c1c; } + +@media (max-width: 768px) { + #barraesquerda { display: none; } + .main { margin-left: 0; } + .month-view { font-size: 12px; } + .dia-box { min-height: 60px; padding: 4px; } +} + +[data-theme="dark"] { + --bg-primary: #121212; + --bg-secondary: #1e1e1e; + --bg-card: #2a2a2a; + --bg-input: #1a1a1a; + --text-primary: #e8e8e8; + --text-secondary: #a0a0a0; + --text-muted: #666; + --border-color: #333; + --border-light: #282828; + --shadow: 0 1px 4px rgba(0,0,0,0.5); + --hover-bg: rgba(255,255,255,0.05); +} + +[data-theme="dark"] body { + background: var(--bg-primary); + color: var(--text-primary); +} + +[data-theme="dark"] .topbar h1 { color: var(--text-primary); } +[data-theme="dark"] .nome { color: var(--text-primary); } +[data-theme="dark"] .cargo { color: var(--text-secondary); } + +[data-theme="dark"] #btnLogout { + border-color: var(--border-color); + color: var(--text-secondary); +} +[data-theme="dark"] #btnLogout:hover { background: rgba(192,57,43,0.2); border-color: #c0392b; color: #e74c3c; } + +[data-theme="dark"] .calendar-header .seta { + border-color: var(--border-color); + color: var(--text-primary); +} +[data-theme="dark"] .calendar-header .seta:hover { background: var(--hover-bg); } +[data-theme="dark"] .titulo-mes { color: var(--text-primary); } + +[data-theme="dark"] .view-switch { background: var(--bg-secondary); } +[data-theme="dark"] .view-switch button { color: var(--text-secondary); } +[data-theme="dark"] .view-switch button.active { + background: var(--bg-card); + color: var(--text-primary); + box-shadow: 0 1px 3px rgba(0,0,0,0.3); +} + +[data-theme="dark"] .calendar-area { + background: var(--bg-secondary); + box-shadow: var(--shadow); +} + +[data-theme="dark"] .dia-semana { + background: var(--bg-card); + color: var(--text-secondary); + border-bottom-color: var(--border-color); +} + +[data-theme="dark"] .dia-box { + border-right-color: var(--border-light); + border-bottom-color: var(--border-light); +} +[data-theme="dark"] .dia-box:hover { background: rgba(192,57,43,0.1); } +[data-theme="dark"] .dia-box.outro-mes { background: var(--bg-primary); color: #444; } +[data-theme="dark"] .dia-box.outro-mes .num-dia { color: #444; } +[data-theme="dark"] .num-dia { color: var(--text-primary); } +[data-theme="dark"] .dia-box.selecionado { background: rgba(192,57,43,0.15); } + +[data-theme="dark"] .week-col { border-right-color: var(--border-color); } +[data-theme="dark"] .week-col.today { background: rgba(192,57,43,0.08); } +[data-theme="dark"] .week-col-head { color: var(--text-secondary); } +[data-theme="dark"] .week-col-date { color: var(--text-muted); } +[data-theme="dark"] .week-empty { color: #444; } + +[data-theme="dark"] .day-panel-header { color: var(--text-primary); } +[data-theme="dark"] .day-empty { color: var(--text-muted); } + +[data-theme="dark"] .calendar-event { background: rgba(192,57,43,0.2); } +[data-theme="dark"] .calendar-event.verde { background: rgba(22,163,74,0.2); } +[data-theme="dark"] .calendar-event.azul { background: rgba(29,78,216,0.2); } +[data-theme="dark"] .calendar-event.amarelo { background: rgba(217,119,6,0.2); } +[data-theme="dark"] .calendar-event.rosa { background: rgba(219,39,119,0.2); } +[data-theme="dark"] .calendar-event-hora { color: var(--text-secondary); } +[data-theme="dark"] .calendar-event-titulo { color: var(--text-primary); } + +[data-theme="dark"] .loading { color: var(--text-muted); } +[data-theme="dark"] .spinner { border-color: var(--border-color); border-top-color: #c0392b; } + +[data-theme="dark"] .modal { + background: var(--bg-secondary); + box-shadow: 0 20px 60px rgba(0,0,0,0.5); +} +[data-theme="dark"] .modal-titulo { color: var(--text-primary); } +[data-theme="dark"] .modal-campo label { color: var(--text-primary); } +[data-theme="dark"] .modal-campo input, +[data-theme="dark"] .modal-campo select, +[data-theme="dark"] .modal-campo textarea { + background: var(--bg-input); + border-color: var(--border-color); + color: var(--text-primary); +} + +[data-theme="dark"] .btn-secundario { + border-color: var(--border-color); + color: var(--text-secondary); +} +[data-theme="dark"] .btn-secundario:hover { background: var(--hover-bg); } + +[data-theme="dark"] .btn-perigo { + border-color: rgba(252,165,165,0.3); + color: #e74c3c; +} +[data-theme="dark"] .btn-perigo:hover { background: rgba(192,57,43,0.2); } + +[data-theme="dark"] #toast { background: #2a2a2a; } + +[data-theme="dark"] .mais-eventos { color: var(--text-secondary); } + +[data-theme="dark"] #btnGerenciarDisciplinas { + background: #c0392b; + color: #fff; +} +[data-theme="dark"] #btnGerenciarDisciplinas:hover { + background: #a03224; +} + +.modal-disc-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.modal-disc-header h2 { + font-size: 18px; + font-weight: 700; + color: #1f2937; + margin: 0; +} +[data-theme="dark"] .modal-disc-header h2 { color: var(--text-primary); } + +.modal-fechar-btn { + background: none; + border: none; + font-size: 22px; + line-height: 1; + cursor: pointer; + color: #9ca3af; + padding: 2px 7px; + border-radius: 6px; + transition: background 0.15s, color 0.15s; +} +.modal-fechar-btn:hover { background: #f3f4f6; color: #374151; } +[data-theme="dark"] .modal-fechar-btn:hover { background: var(--hover-bg); color: var(--text-primary); } + +.modal-corpo-lista { + max-height: 310px; + overflow-y: auto; + margin-bottom: 4px; +} +.modal-corpo-form { margin-bottom: 4px; } + +.disc-item { + display: flex; + align-items: center; + gap: 10px; + padding: 11px 8px; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s; + border-bottom: 1px solid #e5e7eb; +} +.disc-item:last-child { border-bottom: none; } +.disc-item:hover { background: #f9fafb; } +[data-theme="dark"] .disc-item { border-bottom-color: var(--border-color); } +[data-theme="dark"] .disc-item:hover { background: var(--hover-bg); } + +.disc-cor { + width: 13px; + height: 13px; + border-radius: 50%; + flex-shrink: 0; +} +.disc-nome { + font-weight: 600; + font-size: 14px; + flex: 1; + color: #111827; +} +[data-theme="dark"] .disc-nome { color: var(--text-primary); } +.disc-info { + font-size: 12px; + color: #6b7280; +} +[data-theme="dark"] .disc-info { color: var(--text-secondary); } +.disc-seta { + color: #9ca3af; + font-size: 20px; + line-height: 1; +} diff --git a/src/main/resources/static/calendario.html b/src/main/resources/static/calendario.html new file mode 100644 index 0000000..c31b6d6 --- /dev/null +++ b/src/main/resources/static/calendario.html @@ -0,0 +1,1329 @@ + + + + + + Focus Agenda + + + + + + + + + + + + +
+
+
+
+
+ + +
+
+ + + + + + + + +
DOMSEGTERQUAQUISEXSAB
+
+ +
+
+
+ +
+
+

Calendario

+
+
+
+
+ Carregando... + - +
+
+
+ +
+
+ Notificacoes + +
+
+
Nenhuma notificacao
+
+
+
+ + Configuracoes + +
+
+ +
+
+ + Janeiro, 2025 + +
+ +
+ + + +
+ + + +
+ +
+
+ + + + + +
+ + + + + + diff --git a/src/main/resources/static/configuracoes.css b/src/main/resources/static/configuracoes.css new file mode 100644 index 0000000..16a3fba --- /dev/null +++ b/src/main/resources/static/configuracoes.css @@ -0,0 +1,333 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + min-height: 100vh; + font-family: 'Poppins', 'Trebuchet MS', Arial, sans-serif; + display: flex; + align-items: center; + justify-content: center; + padding: 80px 20px 20px; + background: #f5f5f5; +} + +#topo { + width: 100%; + height: 50px; + position: fixed; + top: 0; left: 0; + background: linear-gradient(to right, #c0392b 47%, #7a4951 73%, #114455 87%); + display: flex; + align-items: center; + z-index: 10; +} + +#textotop { + padding-left: 20px; + font-size: clamp(22px, 5vw, 38px); + color: #fff; +} + +#voltar { + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + background: rgba(255,255,255,0.15); + border: 1px solid rgba(255,255,255,0.3); + color: #fff; + padding: 6px 16px; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + font-family: inherit; + transition: background 0.2s; +} + +#voltar:hover { background: rgba(255,255,255,0.3); } + +.theme-toggle-btn { + position: absolute; + right: 140px; + top: 50%; + transform: translateY(-50%); + background: rgba(255,255,255,0.15); + border: 1px solid rgba(255,255,255,0.3); + width: 36px; + height: 36px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s, transform 0.2s; + padding: 4px; +} + +.theme-toggle-btn:hover { background: rgba(255,255,255,0.3); transform: translateY(-50%) scale(1.1); } +.theme-icon { width: 100%; height: 100%; object-fit: contain; } + +#log { + width: 100%; + max-width: 440px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.titulo-pagina { text-align: center; color: #1f2937; } +.subtitulo { text-align: center; color: #6b7280; font-weight: 400; font-size: 15px; } + +.campo { display: flex; flex-direction: column; gap: 8px; } +label { font-weight: 700; color: #1f2937; } + +input, select { + height: 46px; + width: 100%; + padding: 10px; + font-size: 16px; + border: 1px solid #c7c7c7; + border-radius: 6px; + font-family: inherit; + background: #fff; +} + +input:disabled { + background: #f3f4f6; + color: #9ca3af; + cursor: not-allowed; +} + +form { display: flex; flex-direction: column; gap: 16px; } + +.linha-dupla { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + +#logbtn { + align-self: center; + width: 50%; + padding: 12px; + font-size: 18px; + font-weight: bold; + background-color: #c0392b; + color: #fff; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +#logbtn:hover { background-color: #a03224; } +#logbtn:disabled { background-color: #ccc; cursor: not-allowed; } + +.secao { + background: #fff; + border-radius: 10px; + padding: 20px; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); + display: flex; + flex-direction: column; + gap: 16px; +} + +.secao-titulo { + font-size: 16px; + font-weight: 700; + color: #1f2937; + padding-bottom: 10px; + border-bottom: 2px solid #f3f4f6; +} + +.secao-descricao { + font-size: 13px; + color: #6b7280; + margin-bottom: 16px; + line-height: 1.6; +} + +.acoes-lgpd { + display: flex; + flex-direction: column; + gap: 12px; +} + +.acao-lgpd { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 16px; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; +} + +.acao-lgpd strong { + font-size: 14px; + color: #111827; + display: block; + margin-bottom: 2px; +} + +.acao-lgpd p { + font-size: 12px; + color: #6b7280; + margin: 0; +} + +.acao-lgpd button, +.btn-link-politica { + flex-shrink: 0; + padding: 8px 16px; + border: 1px solid #c0392b; + border-radius: 6px; + background: transparent; + color: #c0392b; + font-size: 13px; + font-family: 'Poppins', sans-serif; + cursor: pointer; + text-decoration: none; + transition: all 0.2s; +} + +.acao-lgpd button:hover, +.btn-link-politica:hover { + background: #c0392b; + color: #fff; +} + +#btnSenha { + align-self: center; + width: 60%; + padding: 12px; + font-size: 16px; + font-weight: bold; + background-color: #114455; + color: #fff; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +#btnSenha:hover { background-color: #0d3644; } +#btnSenha:disabled { background-color: #ccc; cursor: not-allowed; } + +.zona-perigo { + background: #fff; + border: 1px solid #fca5a5; + border-radius: 10px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.zona-perigo .secao-titulo { + color: #b91c1c; + border-bottom-color: #fee2e2; +} + +.zona-perigo p { + font-size: 13px; + color: #6b7280; + line-height: 1.5; +} + +#btnExcluirConta { + align-self: center; + width: 60%; + padding: 12px; + font-size: 16px; + font-weight: bold; + background-color: transparent; + color: #b91c1c; + border: 2px solid #b91c1c; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; +} + +#btnExcluirConta:hover { background-color: #fee2e2; } +#btnExcluirConta:disabled { background-color: #ccc; border-color: #ccc; color: #fff; cursor: not-allowed; } + +#mensagem-erro { + background: #fee2e2; + border: 1px solid #fca5a5; + color: #b91c1c; + padding: 10px 14px; + border-radius: 6px; + font-size: 14px; + display: none; +} + +#mensagem-sucesso { + background: #d1fae5; + border: 1px solid #6ee7b7; + color: #065f46; + padding: 10px 14px; + border-radius: 6px; + font-size: 14px; + display: none; +} + +[data-theme="dark"] body { + background: #121212; + color: #e8e8e8; +} + +[data-theme="dark"] .titulo-pagina { color: #e8e8e8; } +[data-theme="dark"] .subtitulo { color: #a0a0a0; } + +[data-theme="dark"] .secao { + background: #1e1e1e; + box-shadow: 0 1px 4px rgba(0,0,0,0.4); +} + +[data-theme="dark"] .secao-titulo { + color: #e8e8e8; + border-bottom-color: #333; +} + +[data-theme="dark"] .secao-descricao { color: #a0a0a0; } + +[data-theme="dark"] label { color: #e8e8e8; } + +[data-theme="dark"] input, +[data-theme="dark"] select { + background: #1a1a1a; + border-color: #333; + color: #e8e8e8; +} + +[data-theme="dark"] input:disabled { + background: #161616; + color: #555; +} + +[data-theme="dark"] .zona-perigo { + background: #1e1e1e; + border-color: rgba(252,165,165,0.2); +} + +[data-theme="dark"] .zona-perigo p { color: #a0a0a0; } + +[data-theme="dark"] #mensagem-erro { + background: rgba(254,226,226,0.1); + border-color: rgba(252,165,165,0.2); + color: #fca5a5; +} + +[data-theme="dark"] #mensagem-sucesso { + background: rgba(209,250,229,0.1); + border-color: rgba(110,231,183,0.2); + color: #6ee7b7; +} + +[data-theme="dark"] a { color: #e8e8e8; } + +[data-theme="dark"] .acao-lgpd { + background: #1a1a1a; + border-color: #333; +} + +[data-theme="dark"] .acao-lgpd strong { color: #e8e8e8; } +[data-theme="dark"] .acao-lgpd p { color: #a0a0a0; } diff --git a/src/main/resources/static/configuracoes.html b/src/main/resources/static/configuracoes.html new file mode 100644 index 0000000..b4e70b6 --- /dev/null +++ b/src/main/resources/static/configuracoes.html @@ -0,0 +1,323 @@ + + + + + + + + + Configuracoes - Focus Agenda + + + +
+

Focus Agenda

+ + +
+ +
+

Configuracoes

+

Gerencie seus dados e preferencias

+ + +
+ +
+
Dados Pessoais
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
Alterar Senha
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ +
+
Seus Dados (LGPD)
+

+ Conforme a Lei Geral de Protecao de Dados (Lei n 13.709/2018), + voce tem direito de acessar, exportar e excluir seus dados pessoais. +

+
+
+
+ Exportar meus dados +

Baixe um arquivo JSON com todas as suas tarefas, eventos e disciplinas.

+
+ +
+
+
+ Politica de Privacidade +

Veja como seus dados sao tratados e quais sao seus direitos.

+
+ Ver politica +
+
+
+ +
+
Zona de Perigo
+

+ Ao excluir sua conta, exercendo o direito previsto no Art. 18, VI da LGPD, + todos os seus dados pessoais (tarefas, eventos, disciplinas e notificacoes) + serao removidos permanentemente e imediatamente dos nossos servidores. + Esta acao e irreversivel. +

+ +
+
+ + + + diff --git a/src/main/resources/static/imagens/engrenagem.png b/src/main/resources/static/imagens/engrenagem.png new file mode 100644 index 0000000000000000000000000000000000000000..108ff4366fa93dc5e29b914313328c8020b82fca GIT binary patch literal 13977 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/static/imagens/moon-svgrepo-com.svg b/src/main/resources/static/imagens/moon-svgrepo-com.svg new file mode 100644 index 0000000..1479a6d --- /dev/null +++ b/src/main/resources/static/imagens/moon-svgrepo-com.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/static/imagens/sino.png b/src/main/resources/static/imagens/sino.png new file mode 100644 index 0000000000000000000000000000000000000000..75756e55ef541912751d7eb0fe890a46f619b4c2 GIT binary patch literal 8791 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 Ib@1W;0|5y5)&Kwi literal 0 HcmV?d00001 diff --git a/src/main/resources/static/imagens/sun-svgrepo-com.svg b/src/main/resources/static/imagens/sun-svgrepo-com.svg new file mode 100644 index 0000000..bf14738 --- /dev/null +++ b/src/main/resources/static/imagens/sun-svgrepo-com.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..c9735df --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,11 @@ + + + + + + Focus Agenda + + + + + diff --git a/src/main/resources/static/login.css b/src/main/resources/static/login.css new file mode 100644 index 0000000..81ec3d5 --- /dev/null +++ b/src/main/resources/static/login.css @@ -0,0 +1,126 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + min-height: 100vh; + font-family: 'Poppins', 'Trebuchet MS', Arial, sans-serif; + display: flex; + align-items: center; + justify-content: center; + padding: 80px 20px 20px; + background: #f5f5f5; +} + +#topo { + width: 100%; + height: 50px; + position: fixed; + top: 0; left: 0; + background: linear-gradient(to right, #c0392b 47%, #7a4951 73%, #114455 87%); + display: flex; + align-items: center; + z-index: 10; +} + +#textotop { + padding-left: 20px; + font-size: clamp(22px, 5vw, 38px); + color: #fff; +} + +#log { + width: 100%; + max-width: 360px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.mens { text-align: center; color: #1f2937; } + +.campo { display: flex; flex-direction: column; gap: 8px; } +label { font-weight: 700; color: #1f2937; } + +input[type="email"], input[type="password"] { + height: 46px; + width: 100%; + padding: 10px; + font-size: 16px; + border: 1px solid #c7c7c7; + border-radius: 6px; + font-family: inherit; +} + +form { display: flex; flex-direction: column; gap: 16px; } + +#logbtn { + align-self: center; + width: 50%; + padding: 12px; + font-size: 18px; + font-weight: bold; + background-color: #c0392b; + color: #fff; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +#logbtn:hover { background-color: #a03224; } +#logbtn:disabled { background-color: #ccc; cursor: not-allowed; } + +a { color: #111; text-decoration: none; } +a:hover { text-decoration: underline; } + +#mensagem-erro { + background: #fee2e2; + border: 1px solid #fca5a5; + color: #b91c1c; + padding: 10px 14px; + border-radius: 6px; + font-size: 14px; + display: none; +} + +.theme-toggle-btn { + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + background: rgba(255,255,255,0.15); + border: 1px solid rgba(255,255,255,0.3); + width: 36px; + height: 36px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s, transform 0.2s; + padding: 4px; +} +.theme-toggle-btn:hover { background: rgba(255,255,255,0.3); transform: translateY(-50%) scale(1.1); } +.theme-icon { width: 100%; height: 100%; object-fit: contain; } + +[data-theme="dark"] body { + background: #121212; + color: #e8e8e8; +} + +[data-theme="dark"] .mens { color: #e8e8e8; } +[data-theme="dark"] label { color: #e8e8e8; } + +[data-theme="dark"] input[type="email"], +[data-theme="dark"] input[type="password"] { + background: #1a1a1a; + border-color: #333; + color: #e8e8e8; +} + +[data-theme="dark"] #mensagem-erro { + background: rgba(254,226,226,0.1); + border-color: rgba(252,165,165,0.2); + color: #fca5a5; +} + +[data-theme="dark"] a { color: #e8e8e8; } diff --git a/src/main/resources/static/login.html b/src/main/resources/static/login.html new file mode 100644 index 0000000..da92ed1 --- /dev/null +++ b/src/main/resources/static/login.html @@ -0,0 +1,104 @@ + + + + + + + + + Login – Focus Agenda + + + +
+

Focus Agenda

+ +
+ +
+

Bem-vindo!

+

Faca seu login

+ + + +
+
+ + +
+
+ + +
+ +
+ +

Cadastrar-se

+
+ + + + diff --git a/src/main/resources/static/politica-privacidade.html b/src/main/resources/static/politica-privacidade.html new file mode 100644 index 0000000..6f0855b --- /dev/null +++ b/src/main/resources/static/politica-privacidade.html @@ -0,0 +1,119 @@ + + + + + + + + + Politica de Privacidade - Focus Agenda + + + + +
+

Focus Agenda

+ +
+ +
+ ← Voltar +

Politica de Privacidade

+

Versao 1.0 - Vigente a partir de maio de 2026

+ +

1. Quem somos

+

FocusAgenda e um sistema de agenda estudantil desenvolvido como Trabalho de Conclusao de Curso (TCC) no curso Tecnico em Desenvolvimento de Sistemas - ETEC Pedro D'Arcadia Neto.

+ +

2. Quais dados coletamos e por que (Art. 9, LGPD)

+
    +
  • Nome completo: identificacao na plataforma
  • +
  • Endereco de email: autenticacao e comunicacao
  • +
  • Senha: armazenada com criptografia BCrypt (nunca em texto simples)
  • +
  • Curso e periodo: personalizao da experiencia
  • +
  • Tarefas, eventos e disciplinas: funcionalidade principal do servico
  • +
+ +

3. Base legal para o tratamento (Art. 7, I, LGPD)

+

Consentimento livre, informado e inequivoco do titular. Ao criar uma conta no FocusAgenda, voce aceita explicitamente o tratamento dos seus dados pessoais conforme descrito nesta politica.

+ +

4. Como protegemos seus dados (Art. 46, LGPD)

+
    +
  • Senhas criptografadas com BCrypt
  • +
  • Comunicacao protegida por HTTPS
  • +
  • Autenticacao via JWT com expiracao de 24 horas
  • +
  • Acesso restrito aos dados do proprio usuario autenticado
  • +
+ +

5. Seus direitos como titular (Art. 18, LGPD)

+
    +
  • Acesso: visualize seus dados em Configuracoes
  • +
  • Portabilidade: exporte todos os seus dados em JSON via Configuracoes
  • +
  • Correcao: edite seus dados em Configuracoes > Dados Pessoais
  • +
  • Eliminacao: exclua sua conta em Configuracoes > Zona de Perigo (todos os dados sao removidos permanentemente e imediatamente)
  • +
+ +

6. Retencao de dados

+

Os dados sao mantidos enquanto a conta estiver ativa. Apos a exclusao da conta, todos os dados sao removidos imediatamente e de forma irreversivel.

+ +

7. Contato

+

Para duvidas sobre privacidade, entre em contato com a equipe do FocusAgenda.

+ +

8. Versao e vigencia

+

Versao 1.0 - vigente a partir de maio de 2026.

+
+ + diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css new file mode 100644 index 0000000..216ef65 --- /dev/null +++ b/src/main/resources/static/style.css @@ -0,0 +1,124 @@ +* { + padding: 0; + margin: 0; +} + +p, +h1, +h2, +h3 { + color: black; + margin: 0; + font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif +} + +label{ +display: block; +} + +body { + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +@media (max-width: 768px) { +body { + padding-left: 20px; + padding-right: 20px; + justify-content: center; +} +} + +@media (max-width: 480px) { +#log { + width: 100% !important; + max-width: 320px; +} +} +#topo { + width: 100%; + height: 50px; + background-color: #111; + position: fixed; + top: 0; + left: 0; + background: linear-gradient(to right, #C0392B 47%, #7A4951 73%, #114455 87%); + display: flex; + align-items: center; + justify-content: flex-start; +} + +#textotop { + padding-left: 20px; + font-size: 38px; + margin: 0; + font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; +} + +#log { + width: 350px; + display: flex; + flex-direction: column; + gap: 15px; + color: white; + margin-top: 70px; +} +#campo { + display: flex; + flex-direction: column; + gap: 15px; + align-self: center; +} +#menslog{ +font-size: 20px; +} + +#emailid, #senhaid { +height: 50px; +width: 100%; +padding: 10px; +font-size: 16px; +border: 1px solid #ccc; +border-radius: 4px; +box-sizing: border-box; +} + +label { +display: block; +margin-bottom: 5px; +font-weight: bold; +color: white; +} + +form { +display: flex; +flex-direction: column; +gap: 15px; +width: 100%; +} + +#logbtn { +align-self: center; +width: 50%; +padding: 12px; +font-size: 18px; +font-weight: bold; +background-color: #C0392B; +color: white; +border: none; +border-radius: 4px; +cursor: pointer; +transition: background-color 0.3s; +} + +#logbtn:hover { +background-color: #A03224; +} +.mens { +align-self: center; +} +#linkcada{ + color: #111; +} \ No newline at end of file diff --git a/src/main/resources/static/theme.js b/src/main/resources/static/theme.js new file mode 100644 index 0000000..9c3ea89 --- /dev/null +++ b/src/main/resources/static/theme.js @@ -0,0 +1,36 @@ +(function() { + var saved = localStorage.getItem('fa_theme'); + var theme = saved || 'light'; + document.documentElement.setAttribute('data-theme', theme); +})(); + +function toggleTheme() { + var current = document.documentElement.getAttribute('data-theme'); + var next = current === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('fa_theme', next); + updateThemeButtons(); +} + +function updateThemeButtons() { + var theme = document.documentElement.getAttribute('data-theme'); + var moonSrc = 'imagens/moon-svgrepo-com.svg'; + var sunSrc = 'imagens/sun-svgrepo-com.svg'; + + var icons = document.querySelectorAll('.theme-icon'); + for (var i = 0; i < icons.length; i++) { + icons[i].src = theme === 'dark' ? sunSrc : moonSrc; + icons[i].alt = theme === 'dark' ? 'Tema Claro' : 'Tema Escuro'; + } + + var btns = document.querySelectorAll('.theme-toggle-btn'); + for (var j = 0; j < btns.length; j++) { + btns[j].title = theme === 'dark' ? 'Tema Claro' : 'Tema Escuro'; + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', updateThemeButtons); +} else { + updateThemeButtons(); +} diff --git a/src/main/resources/static/utils.js b/src/main/resources/static/utils.js new file mode 100644 index 0000000..466999d --- /dev/null +++ b/src/main/resources/static/utils.js @@ -0,0 +1,83 @@ +async function apiFetch(url, options = {}) { + const token = localStorage.getItem('fa_token'); + const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) }; + if (token) headers['Authorization'] = 'Bearer ' + token; + + const res = await fetch(url, { ...options, headers }); + + if (res.status === 401) { + localStorage.clear(); + window.location.href = '/login.html?sessao=expirada'; + return; + } + + const data = await res.json(); + if (!res.ok) throw new Error(data.message || 'Erro desconhecido'); + return data; +} + +function showToast(mensagem, tipo = 'info') { + const existing = document.querySelector('.fa-toast'); + if (existing) existing.remove(); + + const styleId = 'fa-toast-style'; + if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + .fa-toast { + position: fixed; bottom: 24px; right: 24px; z-index: 99999; + padding: 12px 18px; border-radius: 8px; font-size: 14px; + font-family: 'Poppins', sans-serif; max-width: 340px; + animation: faSlideIn 0.3s ease forwards; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + } + .fa-toast.fa-toast-sai { animation: faSlideOut 0.3s ease forwards; } + .fa-toast-sucesso { background: #114455; color: #fff; } + .fa-toast-erro { background: #c0392b; color: #fff; } + .fa-toast-info { background: #f5f5f5; color: #1f2937; border: 1px solid #c0392b; } + @keyframes faSlideIn { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } + @keyframes faSlideOut { from { transform: translateY(0); opacity: 1; } to { transform: translateY(30px); opacity: 0; } } + `; + document.head.appendChild(style); + } + + const toast = document.createElement('div'); + toast.className = 'fa-toast fa-toast-' + tipo; + toast.textContent = mensagem; + document.body.appendChild(toast); + + setTimeout(() => { + toast.classList.add('fa-toast-sai'); + toast.addEventListener('animationend', () => toast.remove()); + }, 3000); +} + +function setButtonLoading(btn, loading, textoOriginal) { + if (loading) { + btn.disabled = true; + btn.dataset.textoOriginal = textoOriginal || btn.textContent; + btn.textContent = 'Salvando...'; + } else { + btn.disabled = false; + if (btn.dataset.textoOriginal) { + btn.textContent = btn.dataset.textoOriginal; + } + } +} + +(function() { + window.addEventListener('offline', () => { + let banner = document.getElementById('fa-offline-banner'); + if (!banner) { + banner = document.createElement('div'); + banner.id = 'fa-offline-banner'; + banner.textContent = 'Sem conexao — algumas funcoes podem nao funcionar'; + banner.style.cssText = 'position:fixed;top:0;left:0;width:100%;background:#c0392b;color:#fff;text-align:center;padding:10px;font-family:Poppins,sans-serif;font-size:14px;z-index:9999;'; + document.body.prepend(banner); + } + }); + window.addEventListener('online', () => { + document.getElementById('fa-offline-banner')?.remove(); + }); +})(); diff --git a/src/test/java/com/agendaestudantil/servico/EstudanteServicoTest.java b/src/test/java/com/agendaestudantil/servico/EstudanteServicoTest.java new file mode 100644 index 0000000..1fa471c --- /dev/null +++ b/src/test/java/com/agendaestudantil/servico/EstudanteServicoTest.java @@ -0,0 +1,107 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RequisicaoCadastroDTO; +import com.agendaestudantil.dto.RespostaEstudanteDTO; +import com.agendaestudantil.dto.RequisicaoLoginDTO; +import com.agendaestudantil.dto.RespostaLoginDTO; +import com.agendaestudantil.entidade.Estudante; +import com.agendaestudantil.excecao.ExcecaoNegocio; +import com.agendaestudantil.repositorio.EstudanteRepositorio; +import com.agendaestudantil.utilitario.UtilJwt; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class EstudanteServicoTest { + + @Mock + private EstudanteRepositorio estudanteRepositorio; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private UtilJwt utilJwt; + + @InjectMocks + private EstudanteServico estudanteServico; + + private RequisicaoCadastroDTO requisicaoCadastro; + private RequisicaoLoginDTO requisicaoLogin; + private Estudante estudante; + + @BeforeEach + void setUp() { + requisicaoCadastro = new RequisicaoCadastroDTO("Teste", "teste@teste.com", "123456", "Curso", 1, true); + requisicaoLogin = new RequisicaoLoginDTO("teste@teste.com", "123456"); + + estudante = new Estudante(); + estudante.setId("1"); + estudante.setNome("Teste"); + estudante.setEmail("teste@teste.com"); + estudante.setSenha("encodedPassword"); + estudante.setCurso("Curso"); + estudante.setPeriodo(1); + } + + @Test + void deveCadastrarEstudanteComSucesso() { + when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.empty()); + when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword"); + when(estudanteRepositorio.save(any(Estudante.class))).thenReturn(estudante); + + RespostaEstudanteDTO resposta = estudanteServico.cadastrar(requisicaoCadastro); + + assertNotNull(resposta); + assertEquals("Teste", resposta.nome()); + verify(estudanteRepositorio, times(1)).save(any(Estudante.class)); + } + + @Test + void deveLancarExcecaoAoCadastrarEmailExistente() { + when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.of(estudante)); + + assertThrows(ExcecaoNegocio.class, () -> estudanteServico.cadastrar(requisicaoCadastro)); + verify(estudanteRepositorio, never()).save(any(Estudante.class)); + } + + @Test + void deveRealizarLoginComSucesso() { + when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.of(estudante)); + when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true); + when(utilJwt.generateToken(anyString())).thenReturn("token123"); + + RespostaLoginDTO resposta = estudanteServico.login(requisicaoLogin); + + assertNotNull(resposta); + assertEquals("token123", resposta.token()); + assertEquals("Teste", resposta.estudante().nome()); + } + + @Test + void deveRejeitarSenhaInvalida() { + when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.of(estudante)); + when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false); + + assertThrows(ResponseStatusException.class, () -> estudanteServico.login(requisicaoLogin)); + } + + @Test + void deveRejeitarEmailNaoEncontrado() { + when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.empty()); + + assertThrows(ResponseStatusException.class, () -> estudanteServico.login(requisicaoLogin)); + } +} diff --git a/src/test/java/com/agendaestudantil/servico/TarefaServicoTest.java b/src/test/java/com/agendaestudantil/servico/TarefaServicoTest.java new file mode 100644 index 0000000..51139d8 --- /dev/null +++ b/src/test/java/com/agendaestudantil/servico/TarefaServicoTest.java @@ -0,0 +1,130 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RequisicaoTarefaDTO; +import com.agendaestudantil.dto.RespostaTarefaDTO; +import com.agendaestudantil.entidade.Tarefa; +import com.agendaestudantil.excecao.ExcecaoNegocio; +import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado; +import com.agendaestudantil.repositorio.TarefaRepositorio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TarefaServicoTest { + + @Mock + private TarefaRepositorio tarefaRepositorio; + + @Mock + private SecurityContext securityContext; + + @Mock + private Authentication authentication; + + @InjectMocks + private TarefaServico tarefaServico; + + private RequisicaoTarefaDTO requisicaoTarefa; + private Tarefa tarefa; + private final String estudanteId = "estudante123"; + + @BeforeEach + void setUp() { + requisicaoTarefa = new RequisicaoTarefaDTO( + "Estudar Spring", + "Aprender Spring Boot 3", + Tarefa.Prioridade.ALTA, + Tarefa.StatusTarefa.PENDENTE, + LocalDate.now(), + null); + + tarefa = new Tarefa(); + tarefa.setId("tarefa1"); + tarefa.setTitulo("Estudar Spring"); + tarefa.setDescricao("Aprender Spring Boot 3"); + tarefa.setPrioridade(Tarefa.Prioridade.ALTA); + tarefa.setStatus(Tarefa.StatusTarefa.PENDENTE); + tarefa.setDataEntrega(LocalDate.now()); + tarefa.setEstudanteId(estudanteId); + } + + private void mockAuthentication(String user) { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getName()).thenReturn(user); + SecurityContextHolder.setContext(securityContext); + } + + @Test + void deveCriarTarefaComSucesso() { + mockAuthentication(estudanteId); + when(tarefaRepositorio.save(any(Tarefa.class))).thenReturn(tarefa); + + RespostaTarefaDTO resposta = tarefaServico.criarTarefa(requisicaoTarefa); + + assertNotNull(resposta); + assertEquals("Estudar Spring", resposta.titulo()); + verify(tarefaRepositorio, times(1)).save(any(Tarefa.class)); + } + + @Test + void deveLancarExcecaoAoCriarTarefaParaOutroUsuario() { + String outroId = "outroEstudante"; + mockAuthentication(outroId); + when(tarefaRepositorio.save(any(Tarefa.class))).thenAnswer(inv -> { + Tarefa t = inv.getArgument(0); + t.setId("tarefa2"); + return t; + }); + + RespostaTarefaDTO resposta = tarefaServico.criarTarefa(requisicaoTarefa); + + assertNotNull(resposta); + assertEquals(outroId, resposta.estudanteId()); + } + + @Test + void deveListarTarefasPorEstudanteComSucesso() { + mockAuthentication(estudanteId); + when(tarefaRepositorio.findByEstudanteId(estudanteId)).thenReturn(List.of(tarefa)); + + List tarefas = tarefaServico.listarTarefasPorEstudante(estudanteId); + + assertFalse(tarefas.isEmpty()); + assertEquals(1, tarefas.size()); + assertEquals("Estudar Spring", tarefas.get(0).titulo()); + } + + @Test + void deveMudarStatusTarefaParaConcluidoComSucesso() { + mockAuthentication(estudanteId); + when(tarefaRepositorio.findById(anyString())).thenReturn(Optional.of(tarefa)); + when(tarefaRepositorio.save(any(Tarefa.class))).thenReturn(tarefa); + + RespostaTarefaDTO resposta = tarefaServico.marcarConcluida("tarefa1"); + + assertNotNull(resposta); + verify(tarefaRepositorio, times(1)).save(any(Tarefa.class)); + } + + @Test + void deveLancarExcecaoQuandoTarefaNaoEncontrada() { + when(tarefaRepositorio.findById(anyString())).thenReturn(Optional.empty()); + + assertThrows(ExcecaoRecursoNaoEncontrado.class, () -> tarefaServico.buscarTarefaPorId("tarefaNaoExistente")); + } +}