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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/src/main/java/com/edtech/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.edtech.dto.RecoveryRequestDto;
import com.edtech.dto.RegisterRequestDto;
import com.edtech.dto.ResetPasswordDto;
import com.edtech.dto.UpdateProfileRequestDto;
import com.edtech.dto.UserResponseDto;
import com.edtech.dto.VerifyCodeDto;
import com.edtech.exception.RateLimitExceededException;
Expand All @@ -22,6 +23,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -259,4 +261,14 @@ public ResponseEntity<UserResponseDto> me(Authentication authentication) {
User user = (User) authentication.getPrincipal();
return ResponseEntity.ok(UserResponseDto.from(user));
}

/** Atualiza os dados de apresentação da conta autenticada. */
@PatchMapping("/me")
public ResponseEntity<UserResponseDto> updateProfile(
@Valid @RequestBody UpdateProfileRequestDto request, Authentication authentication) {
User user = (User) authentication.getPrincipal();
user.setName(request.name().trim());
user.setAvatarUrl(request.avatarUrl());
return ResponseEntity.ok(UserResponseDto.from(userService.saveUserWithoutHash(user)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.util.Map;
import java.util.Optional;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -31,8 +31,8 @@ public LaboratoryController(

/** Javadoc. */
@GetMapping("/token")
public ResponseEntity<?> getLaboratoryToken(@AuthenticationPrincipal UserDetails userDetails) {
Optional<User> userOpt = userRepository.findByEmailIgnoreCase(userDetails.getUsername());
public ResponseEntity<?> getLaboratoryToken(Authentication authentication) {
Optional<User> userOpt = getCurrentUser(authentication);
if (userOpt.isEmpty() || userOpt.get().getRole() != UserRole.ADVISOR) {
return ResponseEntity.status(403)
.body(Map.of("error", "Apenas orientadores podem gerar tokens do laboratorio"));
Expand All @@ -52,13 +52,13 @@ public ResponseEntity<?> getLaboratoryToken(@AuthenticationPrincipal UserDetails
/** Javadoc. */
@PostMapping("/join")
public ResponseEntity<?> joinLaboratory(
@AuthenticationPrincipal UserDetails userDetails, @RequestBody Map<String, String> request) {
Authentication authentication, @RequestBody Map<String, String> request) {
String token = request.get("token");
if (token == null || token.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "O campo token e obrigatorio"));
}

Optional<User> currentUserOpt = userRepository.findByEmailIgnoreCase(userDetails.getUsername());
Optional<User> currentUserOpt = getCurrentUser(authentication);
if (currentUserOpt.isEmpty()) {
return ResponseEntity.status(401).build();
}
Expand All @@ -82,4 +82,18 @@ public ResponseEntity<?> joinLaboratory(
"advisor_name", advisor.getName(),
"institution_id", advisor.getInstitutionId()));
}

private Optional<User> getCurrentUser(Authentication authentication) {
if (authentication == null) {
return Optional.empty();
}
Object principal = authentication.getPrincipal();
if (principal instanceof User user) {
return Optional.of(user);
}
if (principal instanceof UserDetails userDetails) {
return userRepository.findByEmailIgnoreCase(userDetails.getUsername());
}
return Optional.empty();
}
}
9 changes: 9 additions & 0 deletions backend/src/main/java/com/edtech/dto/ProjectResponseDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class ProjectResponseDto {
private String description;
private UUID advisorId;
private ZonedDateTime createdAt;
private boolean member;

// Getters and Setters
/** Documentação para o método getId. */
Expand Down Expand Up @@ -61,4 +62,12 @@ public ZonedDateTime getCreatedAt() {
public void setCreatedAt(ZonedDateTime createdAt) {
this.createdAt = createdAt;
}

public boolean isMember() {
return member;
}

public void setMember(boolean member) {
this.member = member;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.edtech.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/** Dados editáveis do perfil autenticado. */
public record UpdateProfileRequestDto(
@NotBlank @Size(max = 120) String name, @Size(max = 2_000_000) String avatarUrl) {}
6 changes: 4 additions & 2 deletions backend/src/main/java/com/edtech/dto/UserResponseDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public record UserResponseDto(
boolean active,
Instant createdAt,
boolean mfaEnabled,
UUID institutionId) {
UUID institutionId,
String avatarUrl) {

/** Documentação para o método from. */
public static UserResponseDto from(User user) {
Expand All @@ -25,6 +26,7 @@ public static UserResponseDto from(User user) {
user.isActive(),
user.getCreatedAt(),
user.isMfaEnabled(),
user.getInstitutionId());
user.getInstitutionId(),
user.getAvatarUrl());
}
}
15 changes: 15 additions & 0 deletions backend/src/main/java/com/edtech/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public class User {
@Column(name = "mfa_secret", length = 32)
private String mfaSecret;

@Column(name = "avatar_url", columnDefinition = "TEXT")
private String avatarUrl;

protected User() {}

/** Documentação para o método User. */
Expand Down Expand Up @@ -109,6 +112,10 @@ public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

/** Documentação para o método getEmail. */
public String getEmail() {
return email;
Expand Down Expand Up @@ -177,4 +184,12 @@ public String getMfaSecret() {
public void setMfaSecret(String mfaSecret) {
this.mfaSecret = mfaSecret;
}

public String getAvatarUrl() {
return avatarUrl;
}

public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ public interface ProjectRepository extends JpaRepository<Project, UUID> {
+ "WHERE pm.user.id = :userId")
org.springframework.data.domain.Page<Project> findProjectsByUserId(
@Param("userId") UUID userId, org.springframework.data.domain.Pageable pageable);

@Query(
"SELECT p FROM Project p WHERE p.advisor.institutionId = :institutionId")
org.springframework.data.domain.Page<Project> findProjectsByInstitutionId(
@Param("institutionId") UUID institutionId,
org.springframework.data.domain.Pageable pageable);
}
104 changes: 78 additions & 26 deletions backend/src/main/java/com/edtech/service/EmailService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package com.edtech.service;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -10,25 +16,27 @@
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/** Documentação para EmailService. */
/** Sends account emails through the Resend HTTPS API in production and SMTP locally. */
@Service
public class EmailService {

private static final Logger logger = LoggerFactory.getLogger(EmailService.class);
private static final URI RESEND_EMAILS_URI = URI.create("https://api.resend.com/emails");

@Autowired private JavaMailSender mailSender;

@Value("${SMTP_FROM:noreply@edtechacademic.com.br}")
private String fromAddress;

/** Documentação. */
@Value("${SMTP_PASSWORD:}")
private String resendApiKey;

/** Sends the password recovery code. */
@Async
public void sendRecoveryEmail(String toEmail, String otpCode) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(resolveFromAddress());
message.setTo(toEmail);
message.setSubject("Código de Recuperação de Senha - EdTech");
message.setText(
sendEmail(
toEmail,
"Código de Recuperação de Senha - EdTech",
"Olá,\n\n"
+ "Você solicitou a recuperação da sua senha na EdTech.\n"
+ "Seu código de segurança (OTP) de 6 dígitos é: "
Expand All @@ -37,25 +45,14 @@ public void sendRecoveryEmail(String toEmail, String otpCode) {
+ "Este código é válido por 15 minutos.\n"
+ "Se você não solicitou isso, ignore este e-mail.\n\n"
+ "Equipe EdTech");

try {
mailSender.send(message);
logger.info("Recovery email dispatched to {}.", toEmail);
} catch (MailException ex) {
logger.warn(
"Não foi possível enviar e-mail real via SMTP "
+ "(verifique credenciais). Usando apenas simulação no console.");
}
}

/** Javadoc. */
/** Sends the registration verification code. */
@Async
public void sendVerificationEmail(String toEmail, String otpCode) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(resolveFromAddress());
message.setTo(toEmail);
message.setSubject("Código de Verificação de Conta - EdTech");
message.setText(
sendEmail(
toEmail,
"Código de Verificação de Conta - EdTech",
"Olá,\n\n"
+ "Bem-vindo à EdTech!\n"
+ "Seu código de verificação (OTP) de 6 dígitos é: "
Expand All @@ -64,17 +61,72 @@ public void sendVerificationEmail(String toEmail, String otpCode) {
+ "Este código é válido por 15 minutos.\n"
+ "Se você não solicitou isso, ignore este e-mail.\n\n"
+ "Equipe EdTech");
}

private void sendEmail(String toEmail, String subject, String text) {
String apiKey = resendApiKey == null ? "" : resendApiKey.trim();
if (apiKey.startsWith("re_")) {
sendWithResendApi(apiKey, toEmail, subject, text);
return;
}

SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(resolveFromAddress());
message.setTo(toEmail);
message.setSubject(subject);
message.setText(text);
try {
mailSender.send(message);
logger.info("Verification email dispatched to {}.", toEmail);
logger.info("Email dispatched through SMTP to {}.", toEmail);
} catch (MailException ex) {
logger.warn(
"Não foi possível enviar e-mail real via SMTP "
+ "(verifique credenciais). Usando apenas simulação no console.");
logger.warn("SMTP email delivery failed for {}.", toEmail, ex);
}
}

private void sendWithResendApi(String apiKey, String toEmail, String subject, String text) {
String payload =
"{\"from\":\""
+ escapeJson(resolveFromAddress())
+ "\",\"to\":[\""
+ escapeJson(toEmail)
+ "\"],\"subject\":\""
+ escapeJson(subject)
+ "\",\"text\":\""
+ escapeJson(text)
+ "\"}";
HttpRequest request =
HttpRequest.newBuilder(RESEND_EMAILS_URI)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8))
.build();
try {
HttpResponse<String> response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 200 && response.statusCode() < 300) {
logger.info("Email dispatched through Resend API to {}.", toEmail);
} else {
logger.warn(
"Resend API rejected email for {} with HTTP status {}.",
toEmail,
response.statusCode());
}
} catch (IOException ex) {
logger.warn("Resend API delivery failed for {}.", toEmail, ex);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
logger.warn("Resend API delivery interrupted for {}.", toEmail, ex);
}
}

private String escapeJson(String value) {
return value
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r");
}

private String resolveFromAddress() {
return fromAddress == null || fromAddress.isBlank()
? "noreply@edtechacademic.com.br"
Expand Down
24 changes: 23 additions & 1 deletion backend/src/main/java/com/edtech/service/ProjectService.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,20 @@ public ProjectResponseDto createProject(ProjectRequestDto request, UUID advisorI
key = "#userId + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
public org.springframework.data.domain.Page<ProjectResponseDto> listProjectsByUser(
UUID userId, org.springframework.data.domain.Pageable pageable) {
return projectRepository.findProjectsByUserId(userId, pageable).map(this::mapToDto);
User user = userRepository.findById(userId).orElse(null);
if (user == null) {
return projectRepository
.findProjectsByUserId(userId, pageable)
.map(project -> mapToDto(project, userId));
}
if (user.getRole() == com.edtech.model.UserRole.ADVISOR) {
return projectRepository
.findProjectsByUserId(userId, pageable)
.map(project -> mapToDto(project, userId));
}
return projectRepository
.findProjectsByInstitutionId(user.getInstitutionId(), pageable)
.map(project -> mapToDto(project, userId));
}

/** Documentação. */
Expand Down Expand Up @@ -124,12 +137,21 @@ public void addMember(UUID projectId, ProjectMemberRequestDto dto, User authenti
}

private ProjectResponseDto mapToDto(Project project) {
return mapToDto(project, null);
}

private ProjectResponseDto mapToDto(Project project, UUID currentUserId) {
ProjectResponseDto dto = new ProjectResponseDto();
dto.setId(project.getId());
dto.setTitle(project.getTitle());
dto.setDescription(project.getDescription());
dto.setAdvisorId(project.getAdvisor().getId());
dto.setCreatedAt(project.getCreatedAt());
dto.setMember(
currentUserId != null
&& projectMemberRepository
.findByProjectIdAndUserId(project.getId(), currentUserId)
.isPresent());
return dto;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT;
Loading