commit 02cfd71cf4d0075701aa28ffec79d5047bc80214 Author: Axel Date: Tue Mar 31 19:21:11 2026 -0300 Novo repositorio do projeto 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/pom.xml b/pom.xml new file mode 100644 index 0000000..2cc2cb7 --- /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.3 + + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + 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..21b28a4 --- /dev/null +++ b/src/main/java/com/agendaestudantil/AgendaDigitalEstudantesApplication.java @@ -0,0 +1,12 @@ +package com.agendaestudantil; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +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..c436c54 --- /dev/null +++ b/src/main/java/com/agendaestudantil/configuracao/ConfiguracaoSeguranca.java @@ -0,0 +1,77 @@ +package com.agendaestudantil.configuracao; + +import com.agendaestudantil.filtro.FiltroJwt; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.util.List; + +@Configuration +@EnableWebSecurity +public class ConfiguracaoSeguranca { + + private final FiltroJwt filtroJwt; + private final String corsAllowedOrigins; + + public ConfiguracaoSeguranca(FiltroJwt filtroJwt, @Value("${cors.allowed.origins}") String corsAllowedOrigins) { + this.filtroJwt = filtroJwt; + this.corsAllowedOrigins = corsAllowedOrigins; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/", "/index.html", "/login.html", "/cadastro.html", + "/favicon.ico", "/imagens/**", + "/*.css", "/*.js", "/*.ico", "/*.png", + "/api/estudantes/cadastro", "/api/estudantes/login", + "/swagger-ui/**", "/v3/api-docs/**") + .permitAll() + .anyRequest().authenticated()) + .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..eb9832f --- /dev/null +++ b/src/main/java/com/agendaestudantil/configuracao/ResourceController.java @@ -0,0 +1,18 @@ +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"; + } +} 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..b27e8d8 --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/DisciplinaControlador.java @@ -0,0 +1,74 @@ +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.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) { + 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, dto.estudanteId()); + return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta)); + } + + @GetMapping("/estudante/{estudanteId}") + public ResponseEntity>> listarPorEstudante( + @PathVariable String estudanteId) { + List disciplinas = disciplinaServico.listarPorEstudante(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(disciplinas)); + } + + @GetMapping("/{id}") + public ResponseEntity> buscarPorId( + @PathVariable String id, + @RequestParam String estudanteId) { + RespostaDisciplinaDTO disciplina = disciplinaServico.buscarPorId(id, estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(disciplina)); + } + + @PutMapping("/{id}") + public ResponseEntity> atualizarDisciplina( + @PathVariable String id, + @Valid @RequestBody RequisicaoDisciplinaDTO dto) { + 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, dto.estudanteId()); + return ResponseEntity.ok(RespostaApi.sucesso(resposta)); + } + + @DeleteMapping("/{id}") + public ResponseEntity> excluirDisciplina( + @PathVariable String id, + @RequestParam String estudanteId) { + 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..5efcb6b --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/EstudanteControlador.java @@ -0,0 +1,44 @@ +package com.agendaestudantil.controlador; + +import com.agendaestudantil.dto.RespostaApi; +import com.agendaestudantil.dto.RequisicaoCadastroDTO; +import com.agendaestudantil.dto.RespostaEstudanteDTO; +import com.agendaestudantil.dto.RequisicaoLoginDTO; +import com.agendaestudantil.dto.RespostaLoginDTO; +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)); + } + + // Retorna dados do usuário autenticado via JWT (sem precisar do ID na URL) + @GetMapping("/me") + public ResponseEntity> me(@AuthenticationPrincipal UserDetails userDetails) { + RespostaEstudanteDTO resposta = estudanteServico.buscarPorId(userDetails.getUsername()); + return ResponseEntity.ok(RespostaApi.sucesso(resposta)); + } +} 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..51b64be --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/EventoControlador.java @@ -0,0 +1,96 @@ +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.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) { + 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, dto.estudanteId()); + return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta)); + } + + @GetMapping("/estudante/{estudanteId}") + public ResponseEntity>> listarPorEstudante( + @PathVariable String estudanteId) { + List eventos = eventoServico.listarPorEstudante(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(eventos)); + } + + @GetMapping("/estudante/{estudanteId}/periodo") + public ResponseEntity>> listarPorPeriodo( + @PathVariable String estudanteId, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime inicio, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime fim) { + List eventos = eventoServico.listarPorPeriodo(estudanteId, inicio, fim); + return ResponseEntity.ok(RespostaApi.sucesso(eventos)); + } + + @GetMapping("/estudante/{estudanteId}/proximos") + public ResponseEntity>> listarProximosEventos( + @PathVariable String estudanteId) { + List eventos = eventoServico.listarProximosEventos(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(eventos)); + } + + @GetMapping("/{id}") + public ResponseEntity> buscarPorId( + @PathVariable String id, + @RequestParam String estudanteId) { + RespostaEventoDTO evento = eventoServico.buscarPorId(id, estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(evento)); + } + + @PutMapping("/{id}") + public ResponseEntity> atualizarEvento( + @PathVariable String id, + @Valid @RequestBody RequisicaoEventoDTO dto) { + 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.atualizarEvento(id, evento, dto.estudanteId()); + return ResponseEntity.ok(RespostaApi.sucesso(resposta)); + } + + @DeleteMapping("/{id}") + public ResponseEntity> excluirEvento( + @PathVariable String id, + @RequestParam String estudanteId) { + eventoServico.excluirEvento(id, estudanteId); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null)); + } +} 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..e078a12 --- /dev/null +++ b/src/main/java/com/agendaestudantil/controlador/TarefaControlador.java @@ -0,0 +1,86 @@ +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.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("/estudante/{estudanteId}") + public ResponseEntity>> listarTarefasPorEstudante( + @PathVariable String estudanteId) { + List tarefas = tarefaServico.listarTarefasPorEstudante(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(tarefas)); + } + + @GetMapping("/estudante/{estudanteId}/pendentes") + public ResponseEntity>> listarTarefasPendentes( + @PathVariable String estudanteId) { + List tarefas = tarefaServico.listarTarefasPendentes(estudanteId); + return ResponseEntity.ok(RespostaApi.sucesso(tarefas)); + } + + @GetMapping("/estudante/{estudanteId}/data") + public ResponseEntity>> listarTarefasPorData( + @PathVariable String estudanteId, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate data) { + List tarefas = tarefaServico.listarTarefasPorData(estudanteId, data); + return ResponseEntity.ok(RespostaApi.sucesso(tarefas)); + } + + @GetMapping("/estudante/{estudanteId}/periodo") + public ResponseEntity>> listarTarefasPorPeriodo( + @PathVariable String estudanteId, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate inicio, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fim) { + 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/RequisicaoCadastroDTO.java b/src/main/java/com/agendaestudantil/dto/RequisicaoCadastroDTO.java new file mode 100644 index 0000000..7c1dee2 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoCadastroDTO.java @@ -0,0 +1,16 @@ +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 +) { +} 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..5c5c61b --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoDisciplinaDTO.java @@ -0,0 +1,11 @@ +package com.agendaestudantil.dto; + +import jakarta.validation.constraints.NotBlank; + +public record RequisicaoDisciplinaDTO( + @NotBlank String nome, + String professor, + String sala, + String cor, + @NotBlank String estudanteId +) {} 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..fd2032b --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoEventoDTO.java @@ -0,0 +1,17 @@ +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, + @NotBlank String estudanteId +) {} 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..e2e2bd9 --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RequisicaoTarefaDTO.java @@ -0,0 +1,19 @@ +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, + @NotBlank(message = "ID do estudante é obrigatório") String estudanteId +) { +} 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/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..82b764d --- /dev/null +++ b/src/main/java/com/agendaestudantil/dto/RespostaEventoDTO.java @@ -0,0 +1,14 @@ +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 +) {} 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/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..10a53d3 --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Disciplina.java @@ -0,0 +1,28 @@ +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.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; + 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..5dbed5c --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Estudante.java @@ -0,0 +1,33 @@ +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; + +@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; +} 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..b584d56 --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Evento.java @@ -0,0 +1,36 @@ +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.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; + private String estudanteId; + private String titulo; + private String descricao; + private TipoEvento tipo; + private String local; + private String disciplinaId; + private LocalDateTime dataHora; + + public enum TipoEvento { + AULA, PROVA, TRABALHO, ESTUDO, EXAME, OUTRO + } +} 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..1c66c7b --- /dev/null +++ b/src/main/java/com/agendaestudantil/entidade/Tarefa.java @@ -0,0 +1,44 @@ +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.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; + 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..a3dc5d2 --- /dev/null +++ b/src/main/java/com/agendaestudantil/excecao/ManipuladorExcecaoGlobal.java @@ -0,0 +1,53 @@ +package com.agendaestudantil.excecao; + +import com.agendaestudantil.dto.RespostaApi; +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 { + + @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) { + return ResponseEntity.status(ex.getStatusCode()) + .body(new RespostaApi<>(null, ex.getReason(), LocalDateTime.now())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGenericException(Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new RespostaApi<>(null, "Erro interno no servidor: " + ex.getMessage(), LocalDateTime.now())); + } +} 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..4d4e9d0 --- /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); + estudanteId = utilJwt.getEstudanteIdFromToken(token); + } + + if (estudanteId != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(estudanteId); + + if (utilJwt.validateToken(token)) { + 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..7443433 --- /dev/null +++ b/src/main/java/com/agendaestudantil/repositorio/DisciplinaRepositorio.java @@ -0,0 +1,11 @@ +package com.agendaestudantil.repositorio; + +import com.agendaestudantil.entidade.Disciplina; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface DisciplinaRepositorio extends MongoRepository { + List findByEstudanteId(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..3b00b25 --- /dev/null +++ b/src/main/java/com/agendaestudantil/repositorio/EventoRepositorio.java @@ -0,0 +1,22 @@ +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); +} 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..cbd6883 --- /dev/null +++ b/src/main/java/com/agendaestudantil/repositorio/TarefaRepositorio.java @@ -0,0 +1,27 @@ +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); +} 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..c512fb3 --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/DisciplinaServico.java @@ -0,0 +1,92 @@ +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 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; + + public DisciplinaServico(DisciplinaRepositorio disciplinaRepositorio) { + this.disciplinaRepositorio = disciplinaRepositorio; + } + + 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()); + 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..953d13f --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/EstudanteServico.java @@ -0,0 +1,90 @@ +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.excecao.ExcecaoRecursoNaoEncontrado; +import com.agendaestudantil.repositorio.EstudanteRepositorio; +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.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; + + public EstudanteServico(EstudanteRepositorio estudanteRepositorio, PasswordEncoder passwordEncoder, + UtilJwt utilJwt) { + this.estudanteRepositorio = estudanteRepositorio; + this.passwordEncoder = passwordEncoder; + this.utilJwt = utilJwt; + } + + public RespostaEstudanteDTO cadastrar(RequisicaoCadastroDTO dto) { + log.debug("Acessando método cadastrar para email: {}", dto.email()); + Optional existente = estudanteRepositorio.findByEmail(dto.email()); + if (existente.isPresent()) { + log.error("Email já cadastrado: {}", dto.email()); + throw new ExcecaoNegocio("Email já 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 salvo = estudanteRepositorio.save(estudante); + return toResponse(salvo); + } + + public RespostaLoginDTO login(RequisicaoLoginDTO dto) { + log.debug("Acessando método 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 não encontrado")); + return toResponse(estudante); + } + + 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..a4ee1b6 --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/EventoServico.java @@ -0,0 +1,111 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RespostaEventoDTO; +import com.agendaestudantil.entidade.Evento; +import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado; +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.time.LocalDateTime; +import java.util.List; + +@Service +public class EventoServico { + + private static final Logger log = LoggerFactory.getLogger(EventoServico.class); + + private final EventoRepositorio eventoRepositorio; + + public EventoServico(EventoRepositorio eventoRepositorio) { + 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 RespostaEventoDTO criarEvento(Evento evento, String estudanteId) { + log.debug("Criando evento para estudante: {}", estudanteId); + validarAcesso(estudanteId); + + evento.setEstudanteId(estudanteId); + Evento salvo = eventoRepositorio.save(evento); + return toDTO(salvo); + } + + public List listarPorEstudante(String estudanteId) { + log.debug("Listando eventos para estudante: {}", estudanteId); + validarAcesso(estudanteId); + + return eventoRepositorio.findByEstudanteId(estudanteId).stream() + .map(this::toDTO) + .toList(); + } + + public List listarPorPeriodo(String estudanteId, LocalDateTime inicio, LocalDateTime fim) { + validarAcesso(estudanteId); + return eventoRepositorio.findByEstudanteIdAndDataHoraBetween(estudanteId, inicio, fim).stream() + .map(this::toDTO) + .toList(); + } + + public List listarProximosEventos(String estudanteId) { + validarAcesso(estudanteId); + return eventoRepositorio.findProximosEventosByEstudanteId(estudanteId, LocalDateTime.now()).stream() + .map(this::toDTO) + .toList(); + } + + public RespostaEventoDTO buscarPorId(String id, String estudanteId) { + Evento evento = getEventoEntity(id); + validarAcesso(evento.getEstudanteId()); + return toDTO(evento); + } + + public RespostaEventoDTO atualizarEvento(String id, Evento eventoAtualizado, String estudanteId) { + Evento evento = getEventoEntity(id); + validarAcesso(evento.getEstudanteId()); + + evento.setTitulo(eventoAtualizado.getTitulo()); + evento.setDescricao(eventoAtualizado.getDescricao()); + evento.setTipo(eventoAtualizado.getTipo()); + evento.setLocal(eventoAtualizado.getLocal()); + evento.setDataHora(eventoAtualizado.getDataHora()); + evento.setDisciplinaId(eventoAtualizado.getDisciplinaId()); + + return toDTO(eventoRepositorio.save(evento)); + } + + public void excluirEvento(String id, String estudanteId) { + Evento evento = getEventoEntity(id); + validarAcesso(evento.getEstudanteId()); + eventoRepositorio.delete(evento); + } + + private Evento getEventoEntity(String id) { + return eventoRepositorio.findById(id) + .orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Evento não encontrado")); + } + + private RespostaEventoDTO toDTO(Evento evento) { + return new RespostaEventoDTO( + evento.getId(), + evento.getEstudanteId(), + evento.getTitulo(), + evento.getDescricao(), + evento.getTipo() != null ? evento.getTipo().name() : null, + evento.getLocal(), + evento.getDisciplinaId(), + evento.getDataHora() + ); + } +} 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..7697c42 --- /dev/null +++ b/src/main/java/com/agendaestudantil/servico/TarefaServico.java @@ -0,0 +1,139 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RequisicaoTarefaDTO; +import com.agendaestudantil.dto.RespostaTarefaDTO; +import com.agendaestudantil.entidade.Tarefa; +import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado; +import com.agendaestudantil.repositorio.TarefaRepositorio; +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.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. Usuário {} tentou acessar recurso do estudante {}", authUser, estudanteId); + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Acesso negado"); + } + } + + private void validarAcessoTarefa(Tarefa tarefa) { + validarAcesso(tarefa.getEstudanteId()); + } + + public RespostaTarefaDTO criarTarefa(RequisicaoTarefaDTO dto) { + log.debug("Criando tarefa para estudante: {}", dto.estudanteId()); + validarAcesso(dto.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(dto.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.findTarefasPendentesByEstudanteId(estudanteId, Tarefa.StatusTarefa.CONCLUIDA).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); + validarAcesso(tarefa.getEstudanteId()); + + 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()); + 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..8fa79a1 --- /dev/null +++ b/src/main/java/com/agendaestudantil/utilitario/UtilJwt.java @@ -0,0 +1,56 @@ +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; + + public UtilJwt(@Value("${jwt.secret}") String secret) { + this.secret = secret; + } + + 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() + 24 * 60 * 60 * 1000L); + + 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..ed5ab75 --- /dev/null +++ b/src/main/resources/application-dev.properties @@ -0,0 +1,6 @@ +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} +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..a815a0e --- /dev/null +++ b/src/main/resources/static/cadastro.css @@ -0,0 +1,96 @@ +* { 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; +} diff --git a/src/main/resources/static/cadastro.html b/src/main/resources/static/cadastro.html new file mode 100644 index 0000000..6804aba --- /dev/null +++ b/src/main/resources/static/cadastro.html @@ -0,0 +1,151 @@ + + + + + + + + + Cadastro – Focus Agenda + + +
+

Focus Agenda

+
+ +
+

Crie Sua Conta

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

Já 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..41b695f --- /dev/null +++ b/src/main/resources/static/calendario.css @@ -0,0 +1,300 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: 'Poppins', sans-serif; + background: #f5f5f5; +} + +/* ===== HEADER ===== */ +#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; } + +/* ===== BARRA ESQUERDA ===== */ +#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; +} + +/* Mini Calendário */ +#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 */ +#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 */ +#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 ===== */ +.main { + margin-left: 280px; + margin-top: 50px; + padding: 24px; + min-height: calc(100vh - 50px); +} + +/* Topbar */ +.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; } + +/* Botão logout */ +#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; } + +/* Calendar header */ +.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); } + +/* Botão novo evento */ +#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; } + +/* Calendar area */ +.calendar-area { background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); overflow: hidden; } + +/* Month view */ +.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 */ +.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 view */ +.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; } + +/* Event cards */ +.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 SPINNER ===== */ +.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 ===== */ +.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 de feedback */ +#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; } + +/* Responsivo */ +@media (max-width: 768px) { + #barraesquerda { display: none; } + .main { margin-left: 0; } + .month-view { font-size: 12px; } + .dia-box { min-height: 60px; padding: 4px; } +} diff --git a/src/main/resources/static/calendario.html b/src/main/resources/static/calendario.html new file mode 100644 index 0000000..abc8f1d --- /dev/null +++ b/src/main/resources/static/calendario.html @@ -0,0 +1,957 @@ + + + + + + Focus Agenda + + + + + + + + + + + +
+
+
+
+
+ + +
+
+ + + + + + + + +
DOMSEGTERQUAQUISEXSAB
+
+ +
+
+
+ + +
+
+

Calendário

+
+
+
+
+ Carregando... + +
+
+ +
+
+ +
+
+ + Janeiro, 2025 + +
+ +
+ + + +
+ + +
+ +
+
+ + + + + + + + +
+ + + + diff --git a/src/main/resources/static/imagens/engrenagem.png b/src/main/resources/static/imagens/engrenagem.png new file mode 100644 index 0000000..108ff43 Binary files /dev/null and b/src/main/resources/static/imagens/engrenagem.png differ diff --git a/src/main/resources/static/imagens/icone.png b/src/main/resources/static/imagens/icone.png new file mode 100644 index 0000000..4eb80c9 Binary files /dev/null and b/src/main/resources/static/imagens/icone.png differ diff --git a/src/main/resources/static/imagens/sino.png b/src/main/resources/static/imagens/sino.png new file mode 100644 index 0000000..75756e5 Binary files /dev/null and b/src/main/resources/static/imagens/sino.png differ 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..9fca6b8 --- /dev/null +++ b/src/main/resources/static/login.css @@ -0,0 +1,83 @@ +* { 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; +} diff --git a/src/main/resources/static/login.html b/src/main/resources/static/login.html new file mode 100644 index 0000000..a7e60ca --- /dev/null +++ b/src/main/resources/static/login.html @@ -0,0 +1,97 @@ + + + + + + + + + Login – Focus Agenda + + +
+

Focus Agenda

+
+ +
+

Bem-vindo!

+

Faça seu login

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

Cadastrar-se

+
+ + + + diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css new file mode 100644 index 0000000..141c5a7 --- /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; /* Compensa a barra fixa do topo */ +} +#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/test/java/com/agendaestudantil/servico/EstudanteServicoTest.java b/src/test/java/com/agendaestudantil/servico/EstudanteServicoTest.java new file mode 100644 index 0000000..a501c29 --- /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); + 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..cf3b160 --- /dev/null +++ b/src/test/java/com/agendaestudantil/servico/TarefaServicoTest.java @@ -0,0 +1,123 @@ +package com.agendaestudantil.servico; + +import com.agendaestudantil.dto.RequisicaoTarefaDTO; +import com.agendaestudantil.dto.RespostaTarefaDTO; +import com.agendaestudantil.entidade.Tarefa; +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 org.springframework.web.server.ResponseStatusException; + +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, + estudanteId); + + 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() { + mockAuthentication("outroEstudante"); + + assertThrows(ResponseStatusException.class, () -> tarefaServico.criarTarefa(requisicaoTarefa)); + verify(tarefaRepositorio, never()).save(any(Tarefa.class)); + } + + @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")); + } +}