diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/OrganizationUseCaseConfig.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/OrganizationUseCaseConfig.java index 86d4ed1..46ae922 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/OrganizationUseCaseConfig.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/OrganizationUseCaseConfig.java @@ -4,12 +4,16 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import net.beetechgroup.beetask.usecase.organization.create.CreateOrganizationUseCase; import net.beetechgroup.beetask.usecase.repository.OrganizationRepository; +import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository; +import net.beetechgroup.beetask.usecase.repository.UserRepository; @ApplicationScoped public class OrganizationUseCaseConfig { @Produces - public CreateOrganizationUseCase createOrganizationUseCase(OrganizationRepository organizationRepository) { - return new CreateOrganizationUseCase(organizationRepository); + public CreateOrganizationUseCase createOrganizationUseCase(OrganizationRepository organizationRepository, + UserRepository userRepository, + UserOrganizationRepository userOrganizationRepository) { + return new CreateOrganizationUseCase(organizationRepository, userRepository, userOrganizationRepository); } } \ No newline at end of file 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 8da2010..3bcfdb1 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 @@ -6,8 +6,10 @@ import jakarta.inject.Inject; 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.repository.UserRepository; import io.smallrye.jwt.auth.principal.JWTParser; +import org.eclipse.microprofile.config.inject.ConfigProperty; @ApplicationScoped public class UserUseCaseConfig { @@ -15,6 +17,10 @@ public class UserUseCaseConfig { @Inject JWTParser jwtParser; + @Inject + @ConfigProperty(name = "mp.jwt.verify.issuer") + String issuer; + @Produces public CreateUserUseCase createUserUseCase(UserRepository userRepository) { return new CreateUserUseCase(userRepository); @@ -22,11 +28,16 @@ public class UserUseCaseConfig { @Produces public LoginUseCase loginUseCase(UserRepository userRepository) { - return new LoginUseCase(userRepository); + return new LoginUseCase(userRepository, issuer); } @Produces public RefreshTokenUseCase refreshTokenUseCase(UserRepository userRepository) { - return new RefreshTokenUseCase(userRepository, jwtParser); + return new RefreshTokenUseCase(userRepository, jwtParser, issuer); + } + + @Produces + public GetUserProfileUseCase getUserProfileUseCase(UserRepository userRepository) { + return new GetUserProfileUseCase(userRepository); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserOrganizationEntityMapper.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserOrganizationEntityMapper.java index ee38027..f2636ce 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserOrganizationEntityMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserOrganizationEntityMapper.java @@ -13,4 +13,13 @@ public class UserOrganizationEntityMapper { domain.setRole(entity.getRole()); return domain; } + + public static UserOrganizationEntity toEntity(UserOrganization domain) { + if (domain == null) return null; + UserOrganizationEntity entity = new UserOrganizationEntity(); + entity.setUser(UserEntityMapper.toEntity(domain.getUser())); + entity.setOrganization(OrganizationEntityMapper.toEntity(domain.getOrganization())); + entity.setRole(domain.getRole()); + return entity; + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserOrganizationRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserOrganizationRepositoryImpl.java index 4817814..2343565 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserOrganizationRepositoryImpl.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserOrganizationRepositoryImpl.java @@ -2,6 +2,7 @@ package net.beetechgroup.beetask.frameworks.persistence.repository; import io.quarkus.hibernate.orm.panache.PanacheRepository; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; import net.beetechgroup.beetask.entities.organization.UserOrganization; import net.beetechgroup.beetask.frameworks.persistence.entities.UserOrganizationEntity; import net.beetechgroup.beetask.frameworks.persistence.mapper.UserOrganizationEntityMapper; @@ -18,4 +19,11 @@ public class UserOrganizationRepositoryImpl implements UserOrganizationRepositor .map(UserOrganizationEntityMapper::toDomain) .toList(); } + + @Override + @Transactional + public void save(UserOrganization userOrganization) { + UserOrganizationEntity entity = UserOrganizationEntityMapper.toEntity(userOrganization); + persist(entity); + } } 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 4bd5885..ad18f42 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 @@ -22,6 +22,12 @@ import net.beetechgroup.beetask.usecase.user.create.CreateUserUseCase; import java.time.LocalDateTime; import java.util.List; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.ws.rs.GET; +import net.beetechgroup.beetask.usecase.user.profile.GetUserProfileUseCase; +import net.beetechgroup.beetask.usecase.user.profile.UserProfileOutput; + @Path("/auth") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @@ -37,6 +43,21 @@ public class AuthController { @Inject RefreshTokenUseCase refreshTokenUseCase; + @Inject + GetUserProfileUseCase getUserProfileUseCase; + + @Inject + SecurityIdentity securityIdentity; + + @Path("/me") + @GET + @Authenticated + @Operation(summary = "Get current user profile", description = "Returns details of the logged in user") + public UserProfileOutput me() { + String email = securityIdentity.getPrincipal().getName(); + return getUserProfileUseCase.execute(email); + } + @Path("/register") @POST @Operation(summary = "Register", description = "Creates a new user account") 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 0b82f07..975012e 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 @@ -1,5 +1,7 @@ package net.beetechgroup.beetask.interfaceadapters.controllers.organization; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; import org.eclipse.microprofile.openapi.annotations.Operation; import jakarta.inject.Inject; import jakarta.ws.rs.POST; @@ -13,12 +15,16 @@ public class OrganizationController { @Inject CreateOrganizationUseCase createOrganizationUseCase; - + + @Inject + SecurityIdentity securityIdentity; @POST + @Authenticated @Operation(summary = "Create organization", description = "Creates a new organization") public CreateOrganizationResponse createOrganization(CreateOrganizationRequest request) { - CreateOrganizationInput input = OrganizationControllerMapper.toCreateOrganizationInput(request); + String userEmail = securityIdentity.getPrincipal().getName(); + CreateOrganizationInput input = OrganizationControllerMapper.toCreateOrganizationInput(request, userEmail); CreateOrganizationOutput output = createOrganizationUseCase.execute(input); return OrganizationControllerMapper.toCreateOrganizationResponse(output); } 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 c996f20..b81b84b 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 @@ -4,8 +4,8 @@ import net.beetechgroup.beetask.usecase.organization.create.CreateOrganizationIn import net.beetechgroup.beetask.usecase.organization.create.CreateOrganizationOutput; public class OrganizationControllerMapper { - public static CreateOrganizationInput toCreateOrganizationInput(CreateOrganizationRequest request) { - return new CreateOrganizationInput(request.name()); + public static CreateOrganizationInput toCreateOrganizationInput(CreateOrganizationRequest request, String userEmail) { + return new CreateOrganizationInput(request.name(), userEmail); } public static CreateOrganizationResponse toCreateOrganizationResponse(CreateOrganizationOutput output) { diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java index a308695..4e526b8 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java @@ -17,7 +17,6 @@ import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; -import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.entities.task.TaskStatus; import net.beetechgroup.beetask.usecase.task.create.CreateTaskInput; import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput; 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 aca3955..21b7604 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 @@ -15,9 +15,11 @@ import java.util.stream.Collectors; public class LoginUseCase { private final UserRepository userRepository; + private final String issuer; - public LoginUseCase(UserRepository userRepository) { + public LoginUseCase(UserRepository userRepository, String issuer) { this.userRepository = userRepository; + this.issuer = issuer; } public LoginOutput execute(LoginInput input) { @@ -39,7 +41,7 @@ public class LoginUseCase { Set groups = new HashSet<>(); userOrganizations.forEach(uo -> groups.add(uo.getRole().name())); - String token = Jwt.issuer("https://beetech.net") + String token = Jwt.issuer(this.issuer) .upn(user.getEmail()) .groups(groups) .claim("name", user.getName()) @@ -55,7 +57,7 @@ public class LoginUseCase { )) .collect(Collectors.toList()); - String refreshToken = Jwt.issuer("https://beetech.net") + String refreshToken = Jwt.issuer(this.issuer) .upn(user.getEmail()) .expiresIn(86400) // 24 hours .sign(); 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 bf80e9d..56d4922 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 @@ -20,10 +20,12 @@ public class RefreshTokenUseCase { private final UserRepository userRepository; private final JWTParser jwtParser; + private final String issuer; - public RefreshTokenUseCase(UserRepository userRepository, JWTParser jwtParser) { + public RefreshTokenUseCase(UserRepository userRepository, JWTParser jwtParser, String issuer) { this.userRepository = userRepository; this.jwtParser = jwtParser; + this.issuer = issuer; } public LoginOutput execute(RefreshTokenInput input) { @@ -46,7 +48,7 @@ public class RefreshTokenUseCase { Set groups = new HashSet<>(); userOrganizations.forEach(uo -> groups.add(uo.getRole().name())); - String newToken = Jwt.issuer("https://beetech.net") + String newToken = Jwt.issuer(this.issuer) .upn(user.getEmail()) .groups(groups) .claim("name", user.getName()) @@ -54,7 +56,7 @@ public class RefreshTokenUseCase { .sign(); // Generate a new refresh token (also a JWT but with longer expiration) - String newRefreshToken = Jwt.issuer("https://beetech.net") + String newRefreshToken = Jwt.issuer(this.issuer) .upn(user.getEmail()) .expiresIn(86400) // 24 hours .sign(); 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 dfaa92b..4a2e1a8 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) { +public record CreateOrganizationInput(String name, String userEmail) { } 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 c247243..4c8363a 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 @@ -1,19 +1,45 @@ package net.beetechgroup.beetask.usecase.organization.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.usecase.repository.OrganizationRepository; +import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository; +import net.beetechgroup.beetask.usecase.repository.UserRepository; + +import java.util.Optional; public class CreateOrganizationUseCase { private final OrganizationRepository organizationRepository; + private final UserRepository userRepository; + private final UserOrganizationRepository userOrganizationRepository; - public CreateOrganizationUseCase(OrganizationRepository organizationRepository) { + public CreateOrganizationUseCase(OrganizationRepository organizationRepository, + UserRepository userRepository, + UserOrganizationRepository userOrganizationRepository) { this.organizationRepository = organizationRepository; + this.userRepository = userRepository; + this.userOrganizationRepository = userOrganizationRepository; } public CreateOrganizationOutput execute(CreateOrganizationInput input) { + Optional userOptional = userRepository.findByEmail(input.userEmail()); + if (userOptional.isEmpty()) { + throw new RuntimeException("User not found: " + input.userEmail()); + } + User user = userOptional.get(); + Organization organization = new Organization(); organization.setName(input.name()); - organizationRepository.saveOrganization(organization); + organization = organizationRepository.saveOrganization(organization); + + UserOrganization userOrganization = new UserOrganization(); + userOrganization.setUser(user); + userOrganization.setOrganization(organization); + userOrganization.setRole(UserOrganizationRole.OWNER); + userOrganizationRepository.save(userOrganization); + return CreateOrganizationMapper.toCreateOrganizationOutput(organization); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserOrganizationRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserOrganizationRepository.java index 2746dd5..4260bbe 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserOrganizationRepository.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserOrganizationRepository.java @@ -5,4 +5,5 @@ import java.util.List; public interface UserOrganizationRepository { List findByUserId(Long userId); + void save(UserOrganization 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 new file mode 100644 index 0000000..a79a0b8 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/GetUserProfileUseCase.java @@ -0,0 +1,45 @@ +package net.beetechgroup.beetask.usecase.user.profile; + +import net.beetechgroup.beetask.entities.User; +import net.beetechgroup.beetask.entities.organization.UserOrganization; +import net.beetechgroup.beetask.usecase.repository.UserRepository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class GetUserProfileUseCase { + + private final UserRepository userRepository; + + public GetUserProfileUseCase(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public UserProfileOutput execute(String email) { + Optional userOptional = userRepository.findByEmail(email); + + if (userOptional.isEmpty()) { + throw new RuntimeException("User not found"); + } + + User user = userOptional.get(); + List userOrganizations = userRepository.findUserOrganizations(user.getId()); + + List orgs = userOrganizations.stream() + .collect(Collectors.groupingBy(uo -> uo.getOrganization().getName())) + .entrySet().stream() + .map(entry -> new UserProfileOutput.OrganizationProfileOutput( + entry.getKey(), + entry.getValue().stream().map(uo -> uo.getRole().name()).collect(Collectors.toList()) + )) + .collect(Collectors.toList()); + + return new UserProfileOutput( + user.getName(), + user.getEmail(), + user.getPhoto() != null ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random", + orgs + ); + } +} 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 new file mode 100644 index 0000000..667f61b --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/profile/UserProfileOutput.java @@ -0,0 +1,15 @@ +package net.beetechgroup.beetask.usecase.user.profile; + +import java.util.List; + +public record UserProfileOutput( + String name, + String email, + String photo, + List organizations +) { + public record OrganizationProfileOutput( + String name, + List roles + ) {} +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 6697cf2..94554bc 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -18,3 +18,8 @@ quarkus.hibernate-orm.database.generation=validate quarkus.smallrye-openapi.path=/openapi quarkus.swagger-ui.path=/swagger quarkus.swagger-ui.always-include=true + +# JWT Security +mp.jwt.verify.issuer=https://beetech.net +smallrye.jwt.sign.key.location=privateKey.pem +mp.jwt.verify.publickey.location=publicKey.pem