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 + + + + + ); +}; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 3c5f281..5f31aeb 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -17,6 +17,7 @@ import { DashboardPage } from "./pages/DashboardPage"; import { OrgDashboardPage } from "./pages/OrgDashboardPage"; import { RoleGate } from "./components/RoleGate"; import { LandingPage } from "./pages/LandingPage"; +import { ProfilePage } from "./pages/ProfilePage"; export const router = createBrowserRouter([ { @@ -103,6 +104,10 @@ export const router = createBrowserRouter([ ), }, + { + path: "profile", + element: , + }, ], }, ]); diff --git a/frontend/src/services/userService.ts b/frontend/src/services/userService.ts new file mode 100644 index 0000000..ce6171c --- /dev/null +++ b/frontend/src/services/userService.ts @@ -0,0 +1,21 @@ +import { apiFetch } from '../lib/api' + +export interface UpdateProfilePayload { + name: string + email: string +} + +export interface UpdateProfileResult { + name: string + email: string + photo: string +} + +export const userService = { + updateProfile: async (payload: UpdateProfilePayload): Promise => { + return apiFetch('/auth/profile', { + method: 'PUT', + body: payload, + }) + }, +}