From b1441285a19a6bb94f4b7eb62d31891994189a15 Mon Sep 17 00:00:00 2001 From: Gabriel Menezes Date: Thu, 5 Nov 2026 14:45:19 -0400 Subject: [PATCH] feat: criar workspace padrao no cadastro de usuario e impedir organizacoes com nomes duplicados --- .../frameworks/config/UserUseCaseConfig.java | 8 +++- .../OrganizationRepositoryImpl.java | 10 +++++ .../controllers/auth/AuthController.java | 2 + .../organization/OrganizationController.java | 2 + .../create/CreateOrganizationUseCase.java | 15 ++++++- .../repository/OrganizationRepository.java | 1 + .../user/create/CreateUserUseCase.java | 41 ++++++++++++++++++- .../V16__add_unique_organization_name.sql | 4 ++ frontend/src/lib/api.ts | 7 +++- 9 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V16__add_unique_organization_name.sql 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 8be50aa..bfbe09b 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 @@ -9,7 +9,9 @@ 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.user.uploadphoto.UploadUserPhotoUseCase; +import net.beetechgroup.beetask.usecase.repository.OrganizationRepository; import net.beetechgroup.beetask.usecase.repository.StorageRepository; +import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository; import net.beetechgroup.beetask.usecase.repository.UserRepository; import io.smallrye.jwt.auth.principal.JWTParser; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -25,8 +27,10 @@ public class UserUseCaseConfig { String issuer; @Produces - public CreateUserUseCase createUserUseCase(UserRepository userRepository) { - return new CreateUserUseCase(userRepository); + public CreateUserUseCase createUserUseCase(UserRepository userRepository, + OrganizationRepository organizationRepository, + UserOrganizationRepository userOrganizationRepository) { + return new CreateUserUseCase(userRepository, organizationRepository, userOrganizationRepository); } @Produces diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/OrganizationRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/OrganizationRepositoryImpl.java index 277b9cf..1cee426 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/OrganizationRepositoryImpl.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/OrganizationRepositoryImpl.java @@ -58,6 +58,16 @@ public class OrganizationRepositoryImpl implements OrganizationRepository, Panac return organization; } + @Override + public Optional findByNameIgnoreCase(String name) { + if (Objects.isNull(name) || name.isBlank()) { + return Optional.empty(); + } + return find("lower(trim(name)) = ?1", name.trim().toLowerCase()) + .firstResultOptional() + .map(OrganizationEntityMapper::toDomain); + } + @Override public long countAll() { return count(); 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 13257a7..349fc10 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 @@ -11,6 +11,7 @@ import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import net.beetechgroup.beetask.usecase.auth.login.LoginInput; import net.beetechgroup.beetask.usecase.auth.login.LoginOutput; import net.beetechgroup.beetask.usecase.auth.login.LoginUseCase; @@ -129,6 +130,7 @@ public class AuthController { @Path("/register") @POST + @Transactional @Operation(summary = "Register", description = "Creates a new user account") public CreateAccountResponse register(CreateAccountRequest request) { LOGGER.infof("Registration requested for email %s", request.email()); diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/OrganizationController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/OrganizationController.java index 5b941f9..bffe485 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/OrganizationController.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/OrganizationController.java @@ -4,6 +4,7 @@ import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; import org.eclipse.microprofile.openapi.annotations.Operation; import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.PATCH; @@ -63,6 +64,7 @@ public class OrganizationController { @POST @Authenticated + @Transactional @Operation(summary = "Create organization", description = "Creates a new organization") public CreateOrganizationResponse createOrganization(CreateOrganizationRequest request) { String userEmail = securityIdentity.getPrincipal().getName(); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationUseCase.java index 733f275..e2666e0 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationUseCase.java @@ -27,6 +27,17 @@ public class CreateOrganizationUseCase { } public CreateOrganizationOutput execute(CreateOrganizationInput input) { + String trimmedName = input.name() != null ? input.name().trim() : ""; + if (trimmedName.isBlank()) { + LOGGER.warnf("Organization creation failed because name is blank"); + throw new IllegalArgumentException("O nome da organização é obrigatório."); + } + + if (organizationRepository.findByNameIgnoreCase(trimmedName).isPresent()) { + LOGGER.warnf("Organization creation rejected because name '%s' already exists", trimmedName); + throw new IllegalStateException("Já existe uma organização com o nome '" + trimmedName + "'."); + } + Optional userOptional = userRepository.findByEmail(input.userEmail()); if (userOptional.isEmpty()) { LOGGER.warnf("Organization creation failed because user %s was not found", input.userEmail()); @@ -34,9 +45,9 @@ public class CreateOrganizationUseCase { } User user = userOptional.get(); - LOGGER.infof("Creating organization '%s' for owner %s", input.name(), input.userEmail()); + LOGGER.infof("Creating organization '%s' for owner %s", trimmedName, input.userEmail()); Organization organization = new Organization(); - organization.setName(input.name()); + organization.setName(trimmedName); organization = organizationRepository.saveOrganization(organization); UserOrganization userOrganization = new UserOrganization(); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/OrganizationRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/OrganizationRepository.java index ab82278..6dd0efb 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/OrganizationRepository.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/OrganizationRepository.java @@ -9,5 +9,6 @@ public interface OrganizationRepository { Organization saveOrganization(Organization organization); List search(String query); Optional findOrganizationById(Long id); + Optional findByNameIgnoreCase(String name); long countAll(); } \ No newline at end of file diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserUseCase.java index 04873a6..2ade8a1 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserUseCase.java @@ -1,6 +1,12 @@ package net.beetechgroup.beetask.usecase.user.create; import net.beetechgroup.beetask.entities.User; +import net.beetechgroup.beetask.entities.organization.Organization; +import net.beetechgroup.beetask.entities.organization.UserOrganization; +import net.beetechgroup.beetask.entities.organization.UserOrganizationRole; +import net.beetechgroup.beetask.entities.organization.UserOrganizationStatus; +import net.beetechgroup.beetask.usecase.repository.OrganizationRepository; +import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository; import net.beetechgroup.beetask.usecase.repository.UserRepository; import org.jboss.logging.Logger; @@ -8,9 +14,15 @@ public class CreateUserUseCase { private static final Logger LOGGER = Logger.getLogger(CreateUserUseCase.class); private final UserRepository userRepository; + private final OrganizationRepository organizationRepository; + private final UserOrganizationRepository userOrganizationRepository; - public CreateUserUseCase(UserRepository userRepository) { + public CreateUserUseCase(UserRepository userRepository, + OrganizationRepository organizationRepository, + UserOrganizationRepository userOrganizationRepository) { this.userRepository = userRepository; + this.organizationRepository = organizationRepository; + this.userOrganizationRepository = userOrganizationRepository; } public CreateUserOutput execute(CreateUserInput input) { @@ -27,6 +39,33 @@ public class CreateUserUseCase { User savedUser = userRepository.save(user); LOGGER.infof("User %d created successfully with email %s", savedUser.getId(), savedUser.getEmail()); + createDefaultWorkspaceForUser(savedUser); + return CreateUserMapper.toCreateUserOutput(savedUser); } + + private void createDefaultWorkspaceForUser(User user) { + String userName = user.getName() != null && !user.getName().isBlank() ? user.getName().trim() : "Meu"; + String baseWorkspaceName = "Workspace de " + userName; + String uniqueWorkspaceName = baseWorkspaceName; + int counter = 2; + + while (organizationRepository.findByNameIgnoreCase(uniqueWorkspaceName).isPresent()) { + uniqueWorkspaceName = baseWorkspaceName + " (" + counter + ")"; + counter++; + } + + LOGGER.infof("Creating initial workspace '%s' for newly registered user %d (%s)", uniqueWorkspaceName, user.getId(), user.getEmail()); + Organization organization = new Organization(); + organization.setName(uniqueWorkspaceName); + organization = organizationRepository.saveOrganization(organization); + + UserOrganization userOrganization = new UserOrganization(); + userOrganization.setUser(user); + userOrganization.setOrganization(organization); + userOrganization.setRole(UserOrganizationRole.OWNER); + userOrganization.setStatus(UserOrganizationStatus.ACTIVE); + userOrganizationRepository.save(userOrganization); + LOGGER.infof("Initial workspace %d ('%s') successfully linked to user %d", organization.getId(), uniqueWorkspaceName, user.getId()); + } } diff --git a/backend/src/main/resources/db/migration/V16__add_unique_organization_name.sql b/backend/src/main/resources/db/migration/V16__add_unique_organization_name.sql new file mode 100644 index 0000000..d39b809 --- /dev/null +++ b/backend/src/main/resources/db/migration/V16__add_unique_organization_name.sql @@ -0,0 +1,4 @@ +-- Add unique index for organization name (case-insensitive and trimmed), ignoring soft-deleted records +CREATE UNIQUE INDEX IF NOT EXISTS uk_organizations_name +ON organizations (lower(trim(name))) +WHERE deleted_at IS NULL; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 33d7ebb..e41e096 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -53,8 +53,11 @@ export async function apiFetch(path: string, options: RequestOptions = {}): P let errorMessage = text || 'Não foi possível concluir a requisição.' try { const parsed = JSON.parse(text) - if (parsed.message) errorMessage = parsed.message - if (parsed.error) errorMessage = parsed.error + if (parsed.message) { + errorMessage = parsed.message + } else if (parsed.error) { + errorMessage = parsed.error + } } catch { // Usar texto puro }