From 3dff934d610e0ba4ca3d4679212ec9da15081a3a Mon Sep 17 00:00:00 2001 From: Gabriel Menezes Date: Thu, 5 Nov 2026 15:22:13 -0400 Subject: [PATCH] feat(org): adicionar visibilidade publica e privada com protecao em buscas e join requests --- .../entities/organization/Organization.java | 9 +++ .../entities/OrganizationEntity.java | 10 +++ .../mapper/OrganizationEntityMapper.java | 2 + .../OrganizationRepositoryImpl.java | 8 +-- .../auth/AuthControllerMapper.java | 2 +- .../controllers/auth/LoginResponse.java | 3 +- .../CreateOrganizationRequest.java | 3 +- .../CreateOrganizationResponse.java | 2 +- .../OrganizationControllerMapper.java | 5 +- .../usecase/auth/login/LoginOutput.java | 3 +- .../usecase/auth/login/LoginUseCase.java | 3 +- .../auth/refresh/RefreshTokenUseCase.java | 3 +- .../create/CreateOrganizationInput.java | 2 +- .../create/CreateOrganizationMapper.java | 2 +- .../create/CreateOrganizationOutput.java | 2 +- .../create/CreateOrganizationUseCase.java | 3 +- .../join/RequestJoinOrganizationUseCase.java | 5 ++ .../user/create/CreateUserUseCase.java | 3 +- .../user/profile/GetUserProfileUseCase.java | 3 +- .../user/profile/UserProfileOutput.java | 3 +- .../V17__add_organization_is_private.sql | 12 ++++ frontend/src/components/layout/Header.tsx | 6 +- frontend/src/components/layout/Sidebar.tsx | 6 +- frontend/src/pages/OrganizationsPage.tsx | 68 ++++++++++++++++++- frontend/src/services/authService.ts | 1 + frontend/src/services/organizationService.ts | 6 +- 26 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V17__add_organization_is_private.sql diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/organization/Organization.java b/backend/src/main/java/net/beetechgroup/beetask/entities/organization/Organization.java index 67f4a08..4dbfe0b 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/organization/Organization.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/organization/Organization.java @@ -7,6 +7,7 @@ import net.beetechgroup.beetask.entities.Project; public class Organization { private Long id; private String name; + private boolean isPrivate; private List projects; private List users; @@ -26,6 +27,14 @@ public class Organization { this.name = name; } + public boolean isPrivate() { + return isPrivate; + } + + public void setPrivate(boolean isPrivate) { + this.isPrivate = isPrivate; + } + public List getProjects() { return projects; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/OrganizationEntity.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/OrganizationEntity.java index 6f9359a..d6f2010 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/OrganizationEntity.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/OrganizationEntity.java @@ -21,6 +21,8 @@ public class OrganizationEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; + @Column(name = "is_private", nullable = false) + private boolean isPrivate; @Column(name = "deleted_at") private java.time.LocalDateTime deletedAt; @OneToMany(mappedBy = "organization") @@ -50,6 +52,14 @@ public class OrganizationEntity { this.name = name; } + public boolean isPrivate() { + return isPrivate; + } + + public void setPrivate(boolean isPrivate) { + this.isPrivate = isPrivate; + } + public java.time.LocalDateTime getDeletedAt() { return deletedAt; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/OrganizationEntityMapper.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/OrganizationEntityMapper.java index 95211fd..99bcf86 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/OrganizationEntityMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/OrganizationEntityMapper.java @@ -10,6 +10,7 @@ public class OrganizationEntityMapper { OrganizationEntity entity = new OrganizationEntity(); entity.setId(organization.getId()); entity.setName(organization.getName()); + entity.setPrivate(organization.isPrivate()); return entity; } @@ -19,6 +20,7 @@ public class OrganizationEntityMapper { Organization organization = new Organization(); organization.setId(entity.getId()); organization.setName(entity.getName()); + organization.setPrivate(entity.isPrivate()); return organization; } } 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 1cee426..d6c25f6 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 @@ -34,17 +34,17 @@ public class OrganizationRepositoryImpl implements OrganizationRepository, Panac @Override public List search(String query) { if (Objects.isNull(query) || query.isBlank()) { - List organizations = findAll().list().stream() + List organizations = find("isPrivate = false").list().stream() .map(OrganizationEntityMapper::toDomain) .toList(); - LOGGER.infof("Loaded %d organizations with empty search query", organizations.size()); + LOGGER.infof("Loaded %d public organizations with empty search query", organizations.size()); return organizations; } - List organizations = find("lower(name) like ?1", "%" + query.toLowerCase() + "%").stream() + List organizations = find("isPrivate = false and lower(name) like ?1", "%" + query.toLowerCase() + "%").stream() .map(OrganizationEntityMapper::toDomain) .toList(); - LOGGER.infof("Loaded %d organizations for search query '%s'", organizations.size(), query); + LOGGER.infof("Loaded %d public organizations for search query '%s'", organizations.size(), query); return organizations; } 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 11d616d..20a2676 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 @@ -42,7 +42,7 @@ public class AuthControllerMapper { output.expiresIn(), output.issuedAt(), output.organizations().stream() - .map(org -> new LoginResponse.Organization(org.id(), org.name(), org.roles())) + .map(org -> new LoginResponse.Organization(org.id(), org.name(), org.roles(), org.isPrivate())) .collect(Collectors.toList()) ); } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginResponse.java index c2f194a..becff18 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginResponse.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginResponse.java @@ -15,6 +15,7 @@ public record LoginResponse( public record Organization( Long id, String name, - List roles + List roles, + boolean isPrivate ) {} } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/CreateOrganizationRequest.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/CreateOrganizationRequest.java index 1ca9e55..592603c 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/CreateOrganizationRequest.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/CreateOrganizationRequest.java @@ -1,5 +1,6 @@ package net.beetechgroup.beetask.interfaceadapters.controllers.organization; public record CreateOrganizationRequest( - String name) { + String name, + Boolean isPrivate) { } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/CreateOrganizationResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/CreateOrganizationResponse.java index 6a401ce..3052c5c 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/CreateOrganizationResponse.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/CreateOrganizationResponse.java @@ -1,4 +1,4 @@ package net.beetechgroup.beetask.interfaceadapters.controllers.organization; -public record CreateOrganizationResponse(Long id, String name) { +public record CreateOrganizationResponse(Long id, String name, boolean isPrivate) { } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/OrganizationControllerMapper.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/OrganizationControllerMapper.java index b81b84b..dff72b1 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/OrganizationControllerMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/organization/OrganizationControllerMapper.java @@ -5,10 +5,11 @@ import net.beetechgroup.beetask.usecase.organization.create.CreateOrganizationOu public class OrganizationControllerMapper { public static CreateOrganizationInput toCreateOrganizationInput(CreateOrganizationRequest request, String userEmail) { - return new CreateOrganizationInput(request.name(), userEmail); + boolean isPrivate = request.isPrivate() != null ? request.isPrivate() : false; + return new CreateOrganizationInput(request.name(), userEmail, isPrivate); } public static CreateOrganizationResponse toCreateOrganizationResponse(CreateOrganizationOutput output) { - return new CreateOrganizationResponse(output.id(), output.name()); + return new CreateOrganizationResponse(output.id(), output.name(), output.isPrivate()); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginOutput.java index 705f4a3..13f4a74 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginOutput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginOutput.java @@ -15,6 +15,7 @@ public record LoginOutput( public record OrganizationOutput( Long id, String name, - List roles + List roles, + boolean isPrivate ) {} } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginUseCase.java index 2d69ea8..94cc2cf 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginUseCase.java @@ -80,7 +80,8 @@ public class LoginUseCase { return new LoginOutput.OrganizationOutput( entry.getKey(), first.getOrganization().getName(), - entry.getValue().stream().map(uo -> uo.getRole().name()).toList() + entry.getValue().stream().map(uo -> uo.getRole().name()).toList(), + first.getOrganization().isPrivate() ); }) .toList(); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenUseCase.java index 9047d3b..06a3978 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenUseCase.java @@ -75,7 +75,8 @@ public class RefreshTokenUseCase { return new LoginOutput.OrganizationOutput( entry.getKey(), first.getOrganization().getName(), - entry.getValue().stream().map(uo -> uo.getRole().name()).toList() + entry.getValue().stream().map(uo -> uo.getRole().name()).toList(), + first.getOrganization().isPrivate() ); }) .toList(); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationInput.java index 4a2e1a8..a7e2b4e 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationInput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationInput.java @@ -1,4 +1,4 @@ package net.beetechgroup.beetask.usecase.organization.create; -public record CreateOrganizationInput(String name, String userEmail) { +public record CreateOrganizationInput(String name, String userEmail, boolean isPrivate) { } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationMapper.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationMapper.java index e5b2c11..e72cd43 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationMapper.java @@ -4,6 +4,6 @@ import net.beetechgroup.beetask.entities.organization.Organization; public class CreateOrganizationMapper { public static CreateOrganizationOutput toCreateOrganizationOutput(Organization organization) { - return new CreateOrganizationOutput(organization.getId(), organization.getName()); + return new CreateOrganizationOutput(organization.getId(), organization.getName(), organization.isPrivate()); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationOutput.java index 43c112e..c603183 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationOutput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/create/CreateOrganizationOutput.java @@ -1,4 +1,4 @@ package net.beetechgroup.beetask.usecase.organization.create; -public record CreateOrganizationOutput(Long id, String name) { +public record CreateOrganizationOutput(Long id, String name, boolean isPrivate) { } 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 e2666e0..85cd20d 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 @@ -45,9 +45,10 @@ public class CreateOrganizationUseCase { } User user = userOptional.get(); - LOGGER.infof("Creating organization '%s' for owner %s", trimmedName, input.userEmail()); + LOGGER.infof("Creating organization '%s' (private=%b) for owner %s", trimmedName, input.isPrivate(), input.userEmail()); Organization organization = new Organization(); organization.setName(trimmedName); + organization.setPrivate(input.isPrivate()); organization = organizationRepository.saveOrganization(organization); UserOrganization userOrganization = new UserOrganization(); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/join/RequestJoinOrganizationUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/join/RequestJoinOrganizationUseCase.java index 5f9785a..26d4169 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/join/RequestJoinOrganizationUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/organization/join/RequestJoinOrganizationUseCase.java @@ -38,6 +38,11 @@ public class RequestJoinOrganizationUseCase { User user = userOptional.get(); Organization organization = orgOptional.get(); + if (organization.isPrivate()) { + LOGGER.warnf("User %s attempted to request join into private organization %d ('%s')", userEmail, organizationId, organization.getName()); + throw new IllegalStateException("Esta organização é privada e não permite solicitações de entrada."); + } + Optional existing = userOrganizationRepository.findByUserAndOrganization(user.getId(), organization.getId()); if (existing.isPresent()) { if (existing.get().getStatus() == UserOrganizationStatus.ACTIVE) { 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 2ade8a1..c939f7f 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 @@ -55,9 +55,10 @@ public class CreateUserUseCase { counter++; } - LOGGER.infof("Creating initial workspace '%s' for newly registered user %d (%s)", uniqueWorkspaceName, user.getId(), user.getEmail()); + LOGGER.infof("Creating initial private workspace '%s' for newly registered user %d (%s)", uniqueWorkspaceName, user.getId(), user.getEmail()); Organization organization = new Organization(); organization.setName(uniqueWorkspaceName); + organization.setPrivate(true); // Workspace pessoal do usuário é sempre privado organization = organizationRepository.saveOrganization(organization); UserOrganization userOrganization = new UserOrganization(); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/GetUserProfileUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/GetUserProfileUseCase.java index e7b7161..28b4947 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/GetUserProfileUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/GetUserProfileUseCase.java @@ -39,7 +39,8 @@ public class GetUserProfileUseCase { return new UserProfileOutput.OrganizationProfileOutput( entry.getKey(), firstUo.getOrganization().getName(), - entry.getValue().stream().map(uo -> uo.getRole().name()).collect(Collectors.toList()) + entry.getValue().stream().map(uo -> uo.getRole().name()).collect(Collectors.toList()), + firstUo.getOrganization().isPrivate() ); }) .collect(Collectors.toList()); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/UserProfileOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/UserProfileOutput.java index a970cb1..03d0b1f 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/UserProfileOutput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/UserProfileOutput.java @@ -12,6 +12,7 @@ public record UserProfileOutput( public record OrganizationProfileOutput( Long id, String name, - List roles + List roles, + boolean isPrivate ) {} } diff --git a/backend/src/main/resources/db/migration/V17__add_organization_is_private.sql b/backend/src/main/resources/db/migration/V17__add_organization_is_private.sql new file mode 100644 index 0000000..d1e77fa --- /dev/null +++ b/backend/src/main/resources/db/migration/V17__add_organization_is_private.sql @@ -0,0 +1,12 @@ +-- Add is_private column to organizations table +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS is_private BOOLEAN NOT NULL DEFAULT false; + +-- Personal organizations must always be private +UPDATE organizations +SET is_private = true +WHERE lower(name) LIKE '%pessoal%' OR lower(name) LIKE 'workspace de%'; + +-- Index for fast search filtering of public organizations +CREATE INDEX IF NOT EXISTS idx_organizations_public_search +ON organizations (lower(name)) +WHERE is_private = false AND deleted_at IS NULL; diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 115aaa4..cce99f7 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -20,6 +20,7 @@ import { Square, Sun, Moon, + Lock, } from 'lucide-react' import { useAuth } from '../../contexts/AuthContext' import { useTheme } from '../../contexts/ThemeContext' @@ -152,7 +153,10 @@ export const Header = ({ onToggleMobileSidebar }: HeaderProps) => { {org.name.slice(0, 2).toUpperCase()}
-

{org.name}

+

+ {org.name} + {org.isPrivate && } +

{org.roles.join(', ')}

diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 17cc34d..683af5b 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -20,6 +20,7 @@ import { User, LogOut, FileText, + Lock, } from 'lucide-react' import { Chip, ChipLabel } from '@heroui/react' import { useAuth } from '../../contexts/AuthContext' @@ -211,7 +212,10 @@ export const Sidebar = ({ isMobileOpen, onCloseMobile }: SidebarProps) => { {org.name.slice(0, 2).toUpperCase()}
-

{org.name}

+

+ {org.name} + {org.isPrivate && } +

{org.roles.join(', ')}

diff --git a/frontend/src/pages/OrganizationsPage.tsx b/frontend/src/pages/OrganizationsPage.tsx index 1f52fdb..7dbb964 100644 --- a/frontend/src/pages/OrganizationsPage.tsx +++ b/frontend/src/pages/OrganizationsPage.tsx @@ -14,6 +14,8 @@ import { Check, Loader2, Inbox, + Lock, + Globe, } from 'lucide-react' import { useAuth } from '../contexts/AuthContext' import { @@ -32,6 +34,7 @@ export const OrganizationsPage = () => { // Create Org states const [newOrgName, setNewOrgName] = useState('') + const [isPrivateOrg, setIsPrivateOrg] = useState(false) const [isCreating, setIsCreating] = useState(false) // Search & Join states @@ -75,11 +78,12 @@ export const OrganizationsPage = () => { setSuccessMessage(null) try { - const created = await organizationService.create(newOrgName.trim()) + const created = await organizationService.create(newOrgName.trim(), isPrivateOrg) await refreshUser() setActiveOrg(created.id) setSuccessMessage(`Workspace "${newOrgName}" criado com sucesso!`) setNewOrgName('') + setIsPrivateOrg(false) setTimeout(() => { navigate('/dashboard') }, 800) @@ -324,6 +328,17 @@ export const OrganizationsPage = () => { Ativo )} + {org.isPrivate ? ( + + + Privado + + ) : ( + + + Público + + )}

Papel:{' '} @@ -642,6 +657,57 @@ export const OrganizationsPage = () => { /> +

+ +
+ + + +
+
+