From 769791b25c38716817dc99598023c5c9c9e02da0 Mon Sep 17 00:00:00 2001
From: Gabriel Santana de Menezes
Date: Thu, 21 May 2026 15:03:45 -0400
Subject: [PATCH] feat: add user profile page with name and email editing
Backend: new UpdateUserProfileUseCase with email uniqueness validation,
PUT /auth/profile endpoint. Frontend: ProfilePage with avatar initials,
inline success/error feedback, and refreshUser() sync after save.
Avatar dropdown in AppShell gains a "Meu Perfil" link.
Co-Authored-By: Claude Sonnet 4.6
---
.../frameworks/config/UserUseCaseConfig.java | 6 +
.../controllers/auth/AuthController.java | 20 +++
.../auth/AuthControllerMapper.java | 10 ++
.../auth/UpdateProfileRequest.java | 3 +
.../auth/UpdateProfileResponse.java | 3 +
.../user/update/UpdateUserProfileInput.java | 3 +
.../user/update/UpdateUserProfileMapper.java | 15 ++
.../user/update/UpdateUserProfileOutput.java | 3 +
.../user/update/UpdateUserProfileUseCase.java | 46 +++++
frontend/src/components/AppShell.tsx | 8 +
frontend/src/pages/ProfilePage.tsx | 169 ++++++++++++++++++
frontend/src/router.tsx | 5 +
frontend/src/services/userService.ts | 21 +++
13 files changed, 312 insertions(+)
create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/UpdateProfileRequest.java
create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/UpdateProfileResponse.java
create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileInput.java
create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileMapper.java
create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileOutput.java
create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileUseCase.java
create mode 100644 frontend/src/pages/ProfilePage.tsx
create mode 100644 frontend/src/services/userService.ts
diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/UserUseCaseConfig.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/UserUseCaseConfig.java
index 3bcfdb1..b9caa12 100644
--- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/UserUseCaseConfig.java
+++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/UserUseCaseConfig.java
@@ -7,6 +7,7 @@ import net.beetechgroup.beetask.usecase.auth.login.LoginUseCase;
import net.beetechgroup.beetask.usecase.auth.refresh.RefreshTokenUseCase;
import net.beetechgroup.beetask.usecase.user.create.CreateUserUseCase;
import net.beetechgroup.beetask.usecase.user.profile.GetUserProfileUseCase;
+import net.beetechgroup.beetask.usecase.user.update.UpdateUserProfileUseCase;
import net.beetechgroup.beetask.usecase.repository.UserRepository;
import io.smallrye.jwt.auth.principal.JWTParser;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -40,4 +41,9 @@ public class UserUseCaseConfig {
public GetUserProfileUseCase getUserProfileUseCase(UserRepository userRepository) {
return new GetUserProfileUseCase(userRepository);
}
+
+ @Produces
+ public UpdateUserProfileUseCase updateUserProfileUseCase(UserRepository userRepository) {
+ return new UpdateUserProfileUseCase(userRepository);
+ }
}
diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthController.java
index 81ad1a4..d47fa44 100644
--- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthController.java
+++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthController.java
@@ -2,6 +2,7 @@ package net.beetechgroup.beetask.interfaceadapters.controllers.auth;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@@ -18,6 +19,9 @@ import net.beetechgroup.beetask.usecase.auth.refresh.RefreshTokenUseCase;
import net.beetechgroup.beetask.usecase.user.create.CreateUserInput;
import net.beetechgroup.beetask.usecase.user.create.CreateUserOutput;
import net.beetechgroup.beetask.usecase.user.create.CreateUserUseCase;
+import net.beetechgroup.beetask.usecase.user.update.UpdateUserProfileInput;
+import net.beetechgroup.beetask.usecase.user.update.UpdateUserProfileOutput;
+import net.beetechgroup.beetask.usecase.user.update.UpdateUserProfileUseCase;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
@@ -45,6 +49,9 @@ public class AuthController {
@Inject
GetUserProfileUseCase getUserProfileUseCase;
+ @Inject
+ UpdateUserProfileUseCase updateUserProfileUseCase;
+
@Inject
SecurityIdentity securityIdentity;
@@ -58,6 +65,19 @@ public class AuthController {
return getUserProfileUseCase.execute(email);
}
+ @Path("/profile")
+ @PUT
+ @Authenticated
+ @Operation(summary = "Update profile", description = "Updates the name and email of the logged in user")
+ public UpdateProfileResponse updateProfile(UpdateProfileRequest request) {
+ String email = securityIdentity.getPrincipal().getName();
+ LOGGER.infof("Profile update requested for user %s", email);
+ UpdateUserProfileInput input = AuthControllerMapper.toUpdateUserProfileInput(request, email);
+ UpdateUserProfileOutput output = updateUserProfileUseCase.execute(input);
+ LOGGER.infof("Profile updated successfully for user, new email: %s", output.email());
+ return AuthControllerMapper.toUpdateProfileResponse(output);
+ }
+
@Path("/register")
@POST
@Operation(summary = "Register", description = "Creates a new user account")
diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthControllerMapper.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthControllerMapper.java
index 2d34111..128bf96 100644
--- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthControllerMapper.java
+++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthControllerMapper.java
@@ -5,6 +5,8 @@ import net.beetechgroup.beetask.usecase.auth.login.LoginOutput;
import net.beetechgroup.beetask.usecase.auth.refresh.RefreshTokenInput;
import net.beetechgroup.beetask.usecase.user.create.CreateUserInput;
import net.beetechgroup.beetask.usecase.user.create.CreateUserOutput;
+import net.beetechgroup.beetask.usecase.user.update.UpdateUserProfileInput;
+import net.beetechgroup.beetask.usecase.user.update.UpdateUserProfileOutput;
import java.util.stream.Collectors;
public class AuthControllerMapper {
@@ -39,4 +41,12 @@ public class AuthControllerMapper {
.collect(Collectors.toList())
);
}
+
+ public static UpdateUserProfileInput toUpdateUserProfileInput(UpdateProfileRequest request, String currentEmail) {
+ return new UpdateUserProfileInput(currentEmail, request.name(), request.email());
+ }
+
+ public static UpdateProfileResponse toUpdateProfileResponse(UpdateUserProfileOutput output) {
+ return new UpdateProfileResponse(output.name(), output.email(), output.photo());
+ }
}
diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/UpdateProfileRequest.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/UpdateProfileRequest.java
new file mode 100644
index 0000000..c06d08d
--- /dev/null
+++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/UpdateProfileRequest.java
@@ -0,0 +1,3 @@
+package net.beetechgroup.beetask.interfaceadapters.controllers.auth;
+
+public record UpdateProfileRequest(String name, String email) {}
diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/UpdateProfileResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/UpdateProfileResponse.java
new file mode 100644
index 0000000..3f2f362
--- /dev/null
+++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/UpdateProfileResponse.java
@@ -0,0 +1,3 @@
+package net.beetechgroup.beetask.interfaceadapters.controllers.auth;
+
+public record UpdateProfileResponse(String name, String email, String photo) {}
diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileInput.java
new file mode 100644
index 0000000..54c3d9e
--- /dev/null
+++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileInput.java
@@ -0,0 +1,3 @@
+package net.beetechgroup.beetask.usecase.user.update;
+
+public record UpdateUserProfileInput(String currentEmail, String name, String newEmail) {}
diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileMapper.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileMapper.java
new file mode 100644
index 0000000..4866f4c
--- /dev/null
+++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileMapper.java
@@ -0,0 +1,15 @@
+package net.beetechgroup.beetask.usecase.user.update;
+
+import net.beetechgroup.beetask.entities.User;
+
+import java.util.Objects;
+
+public class UpdateUserProfileMapper {
+
+ public static UpdateUserProfileOutput toOutput(User user) {
+ String photo = Objects.nonNull(user.getPhoto())
+ ? user.getPhoto()
+ : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random";
+ return new UpdateUserProfileOutput(user.getName(), user.getEmail(), photo);
+ }
+}
diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileOutput.java
new file mode 100644
index 0000000..692ef57
--- /dev/null
+++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileOutput.java
@@ -0,0 +1,3 @@
+package net.beetechgroup.beetask.usecase.user.update;
+
+public record UpdateUserProfileOutput(String name, String email, String photo) {}
diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileUseCase.java
new file mode 100644
index 0000000..912333a
--- /dev/null
+++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/update/UpdateUserProfileUseCase.java
@@ -0,0 +1,46 @@
+package net.beetechgroup.beetask.usecase.user.update;
+
+import net.beetechgroup.beetask.entities.User;
+import net.beetechgroup.beetask.usecase.repository.UserRepository;
+import org.jboss.logging.Logger;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class UpdateUserProfileUseCase {
+ private static final Logger LOGGER = Logger.getLogger(UpdateUserProfileUseCase.class);
+
+ private final UserRepository userRepository;
+
+ public UpdateUserProfileUseCase(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ public UpdateUserProfileOutput execute(UpdateUserProfileInput input) {
+ User user = userRepository.findByEmail(input.currentEmail())
+ .orElseThrow(() -> {
+ LOGGER.warnf("Profile update failed: user %s not found", input.currentEmail());
+ return new RuntimeException("User not found");
+ });
+
+ if (!input.currentEmail().equals(input.newEmail())) {
+ boolean emailTaken = userRepository.findByEmail(input.newEmail()).isPresent();
+ if (emailTaken) {
+ LOGGER.warnf("Profile update rejected: email %s already in use", input.newEmail());
+ throw new RuntimeException("Email já está em uso por outro usuário");
+ }
+ }
+
+ user.setName(input.name());
+ user.setEmail(input.newEmail());
+ User saved = userRepository.save(user);
+
+ LOGGER.infof("Profile updated for user %d: name=%s, email=%s", saved.getId(), saved.getName(), saved.getEmail());
+
+ String photo = Objects.nonNull(saved.getPhoto())
+ ? saved.getPhoto()
+ : "https://ui-avatars.com/api/?name=" + saved.getName().replace(" ", "+") + "&background=random";
+
+ return new UpdateUserProfileOutput(saved.getName(), saved.getEmail(), photo);
+ }
+}
diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx
index 8de0282..aa6521e 100644
--- a/frontend/src/components/AppShell.tsx
+++ b/frontend/src/components/AppShell.tsx
@@ -422,6 +422,14 @@ export function AppShell() {
{user?.email}
+ setShowAvatarDropdown(false)}
+ className="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-bold text-text-muted hover:bg-surface-muted hover:text-text-main transition-all"
+ >
+
+ Meu Perfil
+