From 7ae81027889a31820c784f93db85ae95b36a237c Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:33:16 +0600 Subject: [PATCH 01/40] refactor(linebreak): remove unnecessary breaks --- src/main/java/io/heapdog/core/security/SecurityUser.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/io/heapdog/core/security/SecurityUser.java b/src/main/java/io/heapdog/core/security/SecurityUser.java index 94bef7d..a5aba61 100644 --- a/src/main/java/io/heapdog/core/security/SecurityUser.java +++ b/src/main/java/io/heapdog/core/security/SecurityUser.java @@ -34,5 +34,4 @@ public String getPassword() { public String getUsername() { return user.getUsername(); } - } From 9cfdbf74aef18bf81b3ed8019e78984858f5c0d6 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:33:48 +0600 Subject: [PATCH 02/40] feat(audit): add lastAccessedAt --- .../io/heapdog/core/feature/serviceuser/ServiceUser.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUser.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUser.java index d3ac9ea..3783124 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUser.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUser.java @@ -5,6 +5,7 @@ import jakarta.persistence.*; import lombok.*; +import java.time.Instant; import java.util.Set; @Builder @@ -29,6 +30,6 @@ public class ServiceUser extends BaseEntity { @Enumerated(EnumType.STRING) private Set permissions; - - + @Column(name = "last_accessed_at") + private Instant lastAccessedAt; } From a5382249fd2d016371135f2011e248fb21d0ed8d Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:19:16 +0600 Subject: [PATCH 03/40] feat(col): add lastAccessedAt column to service user table --- ...44314__add_column_last_accessed_at_to_service_user_table.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/resources/db/migration/V20251212044314__add_column_last_accessed_at_to_service_user_table.sql diff --git a/src/main/resources/db/migration/V20251212044314__add_column_last_accessed_at_to_service_user_table.sql b/src/main/resources/db/migration/V20251212044314__add_column_last_accessed_at_to_service_user_table.sql new file mode 100644 index 0000000..d7fb457 --- /dev/null +++ b/src/main/resources/db/migration/V20251212044314__add_column_last_accessed_at_to_service_user_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE heapdog_service_user + ADD COLUMN last_accessed_at TIMESTAMP WITHOUT TIME ZONE; \ No newline at end of file From 761e220d9547cf2ed3991b5190aba16494661074 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:20:15 +0600 Subject: [PATCH 04/40] feat(audit): update last access timestamp --- .../heapdog/core/security/ApiKeyAuthenticationProvider.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/io/heapdog/core/security/ApiKeyAuthenticationProvider.java b/src/main/java/io/heapdog/core/security/ApiKeyAuthenticationProvider.java index e2699ef..693b688 100644 --- a/src/main/java/io/heapdog/core/security/ApiKeyAuthenticationProvider.java +++ b/src/main/java/io/heapdog/core/security/ApiKeyAuthenticationProvider.java @@ -25,6 +25,11 @@ public Authentication authenticate(Authentication authentication) throws Authent Optional user = repository.findByApiKey(apiKey); if (user.isPresent()) { ServiceUser serviceUser = user.get(); + + // Update last accessed time + serviceUser.setLastAccessedAt(java.time.Instant.now()); + repository.save(serviceUser); + if (!serviceUser.isEnabled()) { throw new BadCredentialsException("API Key is disabled"); } From c3a6b3881058a37269abe31015a1e28d8aba9ad2 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:20:30 +0600 Subject: [PATCH 05/40] refactor(permission): use label instead of description --- .../io/heapdog/core/security/ApiKeyAuthenticationProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/heapdog/core/security/ApiKeyAuthenticationProvider.java b/src/main/java/io/heapdog/core/security/ApiKeyAuthenticationProvider.java index 693b688..7f87d3f 100644 --- a/src/main/java/io/heapdog/core/security/ApiKeyAuthenticationProvider.java +++ b/src/main/java/io/heapdog/core/security/ApiKeyAuthenticationProvider.java @@ -36,7 +36,7 @@ public Authentication authenticate(Authentication authentication) throws Authent return ApiKeyAuthenticationToken.authenticated( serviceUser, serviceUser.getPermissions().stream() - .map(permission -> (GrantedAuthority) permission::name) + .map(permission -> (GrantedAuthority) permission::getLabel) .toList() ); } else { From 1d5cdc0c3b87dc80317de88c97dbe09cf06ee50f Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:21:53 +0600 Subject: [PATCH 06/40] feat(sse-token): implement sse-token obtain system --- .../core/feature/sse/SseController.java | 28 +++++++++++++++ .../io/heapdog/core/feature/sse/SseToken.java | 11 ++++++ .../core/feature/sse/SseTokenRegistry.java | 35 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 src/main/java/io/heapdog/core/feature/sse/SseController.java create mode 100644 src/main/java/io/heapdog/core/feature/sse/SseToken.java create mode 100644 src/main/java/io/heapdog/core/feature/sse/SseTokenRegistry.java diff --git a/src/main/java/io/heapdog/core/feature/sse/SseController.java b/src/main/java/io/heapdog/core/feature/sse/SseController.java new file mode 100644 index 0000000..413b833 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/sse/SseController.java @@ -0,0 +1,28 @@ +package io.heapdog.core.feature.sse; + + +import io.heapdog.core.feature.user.HeapDogUser; +import io.heapdog.core.security.SecurityUser; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/sse-token") +@RequiredArgsConstructor +public class SseController { + + private final SseTokenRegistry sseTokenRegistry; + + @GetMapping("/obtain") + SseToken obtainToken(Authentication authentication) { + HeapDogUser user = ((SecurityUser) authentication.getPrincipal()).getUser(); + String token = sseTokenRegistry.createToken(user.getId()); + return SseToken.builder() + .token(token) + .build(); + } + +} diff --git a/src/main/java/io/heapdog/core/feature/sse/SseToken.java b/src/main/java/io/heapdog/core/feature/sse/SseToken.java new file mode 100644 index 0000000..9f8db66 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/sse/SseToken.java @@ -0,0 +1,11 @@ +package io.heapdog.core.feature.sse; + +import lombok.*; + +@Data +@Builder +public class SseToken { + + private String token; + +} diff --git a/src/main/java/io/heapdog/core/feature/sse/SseTokenRegistry.java b/src/main/java/io/heapdog/core/feature/sse/SseTokenRegistry.java new file mode 100644 index 0000000..bc9637a --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/sse/SseTokenRegistry.java @@ -0,0 +1,35 @@ +package io.heapdog.core.feature.sse; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Component +@Slf4j +public class SseTokenRegistry { + + private final Map tokenToUserId = new ConcurrentHashMap<>(); + + + public String createToken(Long userId) { + String token = UUID.randomUUID().toString(); + tokenToUserId.put(token, userId); + return token; + } + + @PreAuthorize("hasAuthority('read:sse_token')") + public Long getUserIdByToken(String token) { + return tokenToUserId.get(token); + } + + public void invalidateToken(String token) { + log.debug("Invalidating SSE token: {}", token); + tokenToUserId.remove(token); + } + +} From a076b0d3f84a23b7fde355839a4e3f72a6e13e86 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:22:42 +0600 Subject: [PATCH 07/40] feat(permission): handle permissions for service user --- .../serviceuser/ServiceUserService.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserService.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserService.java index d608e19..de5f1da 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserService.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserService.java @@ -1,10 +1,13 @@ package io.heapdog.core.feature.serviceuser; +import io.heapdog.core.shared.ResourceNotFoundException; import io.heapdog.core.shared.util.OtpGenerator; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; +import java.util.Arrays; + @Service @RequiredArgsConstructor public class ServiceUserService { @@ -17,6 +20,7 @@ ServiceUserCreateResponseDto createServiceUser(ServiceUserCreateRequestDto reque .name(request.getName()) .apiKey(String.format("svc-%s", OtpGenerator.generateOtp(32))) .enabled(true) + .permissions(request.getPermissions()) .build(); var saved = serviceUserRepository.save(serviceUser); return ServiceUserCreateResponseDto.builder() @@ -24,6 +28,43 @@ ServiceUserCreateResponseDto createServiceUser(ServiceUserCreateRequestDto reque .name(saved.getName()) .apiKey(saved.getApiKey()) .enabled(saved.isEnabled()) + .permissions(saved.getPermissions()) + .build(); + } + + + @PreAuthorize("hasRole('ADMIN')") + ServiceUserReadResponseDto getServiceUser(Long id) { + var serviceUser = serviceUserRepository.findByIdWithPermissionsAndCreator(id) + .orElseThrow(() -> new ResourceNotFoundException("ServiceUser", "id", id.toString())); + return ServiceUserReadResponseDto.builder() + .id(serviceUser.getId()) + .name(serviceUser.getName()) + .apiKey(serviceUser.getApiKey()) + .enabled(serviceUser.isEnabled()) + .createdAt(serviceUser.getCreatedAt()) + .updatedAt(serviceUser.getUpdatedAt()) + .createdBy(ServiceUserReadResponseDto.User.builder() + .id(serviceUser.getCreatedBy().getId()) + .username(serviceUser.getCreatedBy().getUsername()) + .build()) + .permissions(serviceUser.getPermissions()) + .lastAccessedAt(serviceUser.getLastAccessedAt()) + .build(); + } + + + @PreAuthorize("hasRole('ADMIN')") + ServiceUserPermissionResponseDto getServiceUserPermissions() { + var permissions = Arrays.stream(ServiceUserPermission.values()) + .map(permission -> ServiceUserPermissionResponseDto.Permission.builder() + .name(permission.name()) + .label(permission.getLabel()) + .description(permission.getDescription()) + .build()) + .toList(); + return ServiceUserPermissionResponseDto.builder() + .permissions(permissions) .build(); } } From 0ff84d503f208ee5a0c44f221843de8ac5f69f39 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:23:23 +0600 Subject: [PATCH 08/40] feat(api): add new api findByIdWithPermissionsAndCreator --- .../feature/serviceuser/ServiceUserRepository.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserRepository.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserRepository.java index 5586bb9..3c15d6c 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserRepository.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserRepository.java @@ -1,6 +1,7 @@ package io.heapdog.core.feature.serviceuser; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.Optional; @@ -8,4 +9,13 @@ public interface ServiceUserRepository extends JpaRepository Optional findByApiKey(String apiKey); + @Query(""" + SELECT DISTINCT s + FROM ServiceUser s + LEFT JOIN FETCH s.permissions + LEFT JOIN FETCH s.createdBy + WHERE s.id = :id + """) + Optional findByIdWithPermissionsAndCreator(Long id); + } From 63253cb18cb0b896580ad4c5099e81f274281ef9 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:24:43 +0600 Subject: [PATCH 09/40] feat(api): add new api to get all memberships (no paging) --- .../feature/organization/MembershipRepository.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/heapdog/core/feature/organization/MembershipRepository.java b/src/main/java/io/heapdog/core/feature/organization/MembershipRepository.java index 74a311a..7f6b4a1 100644 --- a/src/main/java/io/heapdog/core/feature/organization/MembershipRepository.java +++ b/src/main/java/io/heapdog/core/feature/organization/MembershipRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.List; import java.util.Optional; public interface MembershipRepository extends JpaRepository { @@ -24,7 +25,8 @@ public interface MembershipRepository extends JpaRepository { m.user.id, m.user.username, m.user.email, - m.role + m.role, + m.id ) FROM Membership m WHERE m.organization.id = :organizationId @@ -33,4 +35,11 @@ SELECT COUNT(m) FROM Membership m WHERE m.organization.id = :organizationId """) Page findByOrganizationId(Long organizationId, Pageable pageable); + + @Query(""" + SELECT m FROM Membership m + JOIN FETCH m.organization + WHERE m.organization.id = :organizationId +""") + List findByOrganizationId(Long organizationId); } From 173561fb00fe712034b1e3cf388129c1fe95b994 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:28:10 +0600 Subject: [PATCH 10/40] feat(internal): implement internal rest communication --- .../InternalNotificationController.java | 23 ++++++++++++++ .../InternalNotificationResponseDto.java | 22 ++++++++++++++ .../notification/NotificationService.java | 17 +++++++++++ .../feature/sse/InternalSseController.java | 30 +++++++++++++++++++ .../InternalSseValidateTokenResponseDto.java | 11 +++++++ .../core/feature/user/HeapDogUserService.java | 13 ++++++++ .../feature/user/InternalUserController.java | 19 ++++++++++++ .../feature/user/InternalUserResponseDto.java | 17 +++++++++++ 8 files changed, 152 insertions(+) create mode 100644 src/main/java/io/heapdog/core/feature/notification/InternalNotificationController.java create mode 100644 src/main/java/io/heapdog/core/feature/notification/InternalNotificationResponseDto.java create mode 100644 src/main/java/io/heapdog/core/feature/sse/InternalSseController.java create mode 100644 src/main/java/io/heapdog/core/feature/sse/InternalSseValidateTokenResponseDto.java create mode 100644 src/main/java/io/heapdog/core/feature/user/InternalUserController.java create mode 100644 src/main/java/io/heapdog/core/feature/user/InternalUserResponseDto.java diff --git a/src/main/java/io/heapdog/core/feature/notification/InternalNotificationController.java b/src/main/java/io/heapdog/core/feature/notification/InternalNotificationController.java new file mode 100644 index 0000000..f438447 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/notification/InternalNotificationController.java @@ -0,0 +1,23 @@ +package io.heapdog.core.feature.notification; + + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/internal/notifications") +@RequiredArgsConstructor +public class InternalNotificationController { + + private final NotificationService notificationService; + + @GetMapping("/{id}") + InternalNotificationResponseDto + getNotifications(@PathVariable Long id) { + return notificationService.getNotificationById(id); + } + +} diff --git a/src/main/java/io/heapdog/core/feature/notification/InternalNotificationResponseDto.java b/src/main/java/io/heapdog/core/feature/notification/InternalNotificationResponseDto.java new file mode 100644 index 0000000..bdc1b17 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/notification/InternalNotificationResponseDto.java @@ -0,0 +1,22 @@ +package io.heapdog.core.feature.notification; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.Instant; + + +@Getter +@Setter +@Builder +public class InternalNotificationResponseDto { + private Long id; + private String message; + private String link; + private boolean read; + private boolean clicked; + private NotificationType type; + private Instant createdAt; + private Long userId; +} diff --git a/src/main/java/io/heapdog/core/feature/notification/NotificationService.java b/src/main/java/io/heapdog/core/feature/notification/NotificationService.java index dc0b0e3..6278457 100644 --- a/src/main/java/io/heapdog/core/feature/notification/NotificationService.java +++ b/src/main/java/io/heapdog/core/feature/notification/NotificationService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import java.util.List; @@ -26,6 +27,22 @@ public Notification createNotification(String message, String link) { return notificationRepository.save(notification); } + @PreAuthorize("hasAuthority('read:notification')") + InternalNotificationResponseDto getNotificationById(Long notificationId) { + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new ResourceNotFoundException("Notification", "id", notificationId.toString())); + return InternalNotificationResponseDto.builder() + .id(notification.getId()) + .message(notification.getMessage()) + .link(notification.getLink()) + .read(notification.isRead()) + .clicked(notification.isClicked()) + .type(notification.getType()) + .createdAt(notification.getCreatedAt()) + .userId(notification.getRecipient().getId()) + .build(); + } + Page getNotifications(Long userId, Long page, Long size) { return notificationRepository.findByRecipientId( diff --git a/src/main/java/io/heapdog/core/feature/sse/InternalSseController.java b/src/main/java/io/heapdog/core/feature/sse/InternalSseController.java new file mode 100644 index 0000000..0265814 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/sse/InternalSseController.java @@ -0,0 +1,30 @@ +package io.heapdog.core.feature.sse; + + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/internal/sse-token") +@RequiredArgsConstructor +public class InternalSseController { + + private final SseTokenRegistry sseTokenRegistry; + + @GetMapping("/validate") + InternalSseValidateTokenResponseDto validateSseToken(@RequestParam String token) { + + Long userId = sseTokenRegistry.getUserIdByToken(token); + if (userId == null) { + throw new IllegalArgumentException("Invalid SSE token"); + } + return InternalSseValidateTokenResponseDto.builder() + .userId(userId) + .build(); + } + + @DeleteMapping("/invalidate") + void invalidateSseToken(@RequestParam String token) { + sseTokenRegistry.invalidateToken(token); + } +} diff --git a/src/main/java/io/heapdog/core/feature/sse/InternalSseValidateTokenResponseDto.java b/src/main/java/io/heapdog/core/feature/sse/InternalSseValidateTokenResponseDto.java new file mode 100644 index 0000000..7f3785c --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/sse/InternalSseValidateTokenResponseDto.java @@ -0,0 +1,11 @@ +package io.heapdog.core.feature.sse; + + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class InternalSseValidateTokenResponseDto { + private Long userId; +} diff --git a/src/main/java/io/heapdog/core/feature/user/HeapDogUserService.java b/src/main/java/io/heapdog/core/feature/user/HeapDogUserService.java index ee761a0..283fa74 100644 --- a/src/main/java/io/heapdog/core/feature/user/HeapDogUserService.java +++ b/src/main/java/io/heapdog/core/feature/user/HeapDogUserService.java @@ -111,4 +111,17 @@ public EmailVerificationResponseDto verifyEmailOtp(EmailVerificationRequestDto d .message("Email verified successfully.") .build(); } + + InternalUserResponseDto getUserById(Long userId) { + HeapDogUser user = repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("User not found with ID: " + userId)); + return InternalUserResponseDto.builder() + .id(user.getId()) + .username(user.getUsername()) + .role(user.getRole().name()) + .enabled(user.getEnabled()) + .build(); + } + + } diff --git a/src/main/java/io/heapdog/core/feature/user/InternalUserController.java b/src/main/java/io/heapdog/core/feature/user/InternalUserController.java new file mode 100644 index 0000000..6e10603 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/user/InternalUserController.java @@ -0,0 +1,19 @@ +package io.heapdog.core.feature.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/internal/users") +@RequiredArgsConstructor +public class InternalUserController { + + private final HeapDogUserService userService; + + @GetMapping("/{userId}") + InternalUserResponseDto getUser(@PathVariable Long userId) { + InternalUserResponseDto user = userService.getUserById(userId); + return user; + } + +} diff --git a/src/main/java/io/heapdog/core/feature/user/InternalUserResponseDto.java b/src/main/java/io/heapdog/core/feature/user/InternalUserResponseDto.java new file mode 100644 index 0000000..aa35e51 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/user/InternalUserResponseDto.java @@ -0,0 +1,17 @@ +package io.heapdog.core.feature.user; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class InternalUserResponseDto { + + private Long id; + private String username; + private String role; + private Boolean enabled; + +} From d55ef58b259b952eaa24123ffd443fd60e1da98f Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:29:23 +0600 Subject: [PATCH 11/40] fix(exception): handle InvalidFormatException inside HttpMessageNotReadableException --- .../core/shared/GlobalControllerAdvice.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main/java/io/heapdog/core/shared/GlobalControllerAdvice.java b/src/main/java/io/heapdog/core/shared/GlobalControllerAdvice.java index cf7c972..fe35e4a 100644 --- a/src/main/java/io/heapdog/core/shared/GlobalControllerAdvice.java +++ b/src/main/java/io/heapdog/core/shared/GlobalControllerAdvice.java @@ -1,6 +1,7 @@ package io.heapdog.core.shared; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import io.heapdog.core.feature.auth.InvalidOtpException; import io.heapdog.core.feature.user.UserAlreadyVerifiedException; import io.heapdog.core.feature.user.UserNotFoundException; @@ -259,6 +260,35 @@ ResponseEntity handleIllegalArgumentException(IllegalArgumentException @ExceptionHandler(exception = HttpMessageNotReadableException.class) ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException ex, HttpServletRequest request) { log.warn(ex.toString()); + + if (ex.getCause() instanceof InvalidFormatException ife && ife.getTargetType().isEnum()) { + String invalidValue = ife.getValue().toString(); + String enumType = ife.getTargetType().getSimpleName(); + String allowedValues = String.join(", ", + List.of(ife.getTargetType().getEnumConstants()).stream() + .map(Object::toString) + .toList() + ); + + ApiError errorResponse = ApiError + .builder() + .timestamp(Instant.now()) + .status(HttpStatus.BAD_REQUEST.value()) + .error(HttpStatus.BAD_REQUEST.getReasonPhrase()) + .code("INVALID_ENUM_VALUE") + .message("Invalid enum value in request body") + .details(List.of( + ApiError.FieldError.builder() + .field(ife.getPath().get(0).getFieldName()) + .message(String.format("Invalid value '%s' for enum '%s'. Allowed values are: %s", + invalidValue, enumType, allowedValues)) + .build() + )) + .path(request.getRequestURI()) + .build(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); + } + ApiError errorResponse = ApiError .builder() .timestamp(Instant.now()) From 6895cbc43db46b0eafa54fa9b39f81e964368d55 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:08:01 +0600 Subject: [PATCH 12/40] feat(api): add api to update member role in an organization --- .../organization/OrganizationService.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index 2ea0c5c..b8a17cc 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -11,6 +11,7 @@ import io.heapdog.core.shared.ResourceNotFoundException; import io.heapdog.core.shared.util.OtpGenerator; import jakarta.transaction.Transactional; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; @@ -292,4 +293,35 @@ public OrganizationInvitationAcceptInfoResponseDto getInvitationAcceptInfo(Strin } return organizationInvitationMapper.toOrganizationInvitationAcceptInfoResponseDto(invitation); } + + public OrganizationMemberResponseDto updateOrganizationMemberRole(String slug, Long membershipId, @Valid OrganizationMemberRoleUpdateRequestDto dto) { + Organization organization = repository.findBySlug(slug) + .orElseThrow(() -> new ResourceNotFoundException("Organization", "slug", slug)); + Membership membership = membershipRepository.findById(membershipId) + .orElseThrow(() -> new ResourceNotFoundException("Membership", "id", membershipId.toString())); + if (!membership.getOrganization().getId().equals(organization.getId())) { + throw new ResourceNotFoundException("Membership", "id", membershipId.toString()); + } + membership.setRole(dto.getRole()); + Membership savedMembership = membershipRepository.save(membership); + + // create notification to the user about role change + Notification notification = Notification.builder() + .message("Your role in the organization " + organization.getOrgName() + " has been changed to " + dto.getRole()) + .link("/organizations/" + organization.getSlug() + "/members") + .recipient(savedMembership.getUser()) + .type(NotificationType.ORGANIZATION_MEMBER_ROLE_UPDATED) + .build(); + Notification savedNotification = notificationRepository.save(notification); + natsPublisher.publishNotificationEvent(savedNotification.getId()); + + var user = savedMembership.getUser(); + return OrganizationMemberResponseDto.builder() + .id(user.getId()) + .username(user.getUsername()) + .email(user.getEmail()) + .role(savedMembership.getRole()) + .membershipId(savedMembership.getId()) + .build(); + } } From ca30a2285a4177b0bd0e37c38666699cf20dce91 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:09:19 +0600 Subject: [PATCH 13/40] fix(dto): include membershipId while building OrganizationMemberResponseDto --- .../heapdog/core/feature/organization/OrganizationService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index b8a17cc..74a3002 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -93,6 +93,7 @@ OrganizationBasicInfoWithMembershipIdResponseDto createOrganization(Organization .username(user.getUsername()) .email(user.getEmail()) .role(m.getRole()) + .membershipId(m.getId()) .build(); }); } From eb3d8cfb17e4f486d0bc4cff81df5d033e8c60c8 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:10:41 +0600 Subject: [PATCH 14/40] fix(notification): send out notification about organization update --- .../feature/organization/OrganizationService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index 74a3002..888930d 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -126,6 +126,18 @@ OrganizationBasicInfoResponseDto updateOrganization(String slug, OrganizationBas organization.setPhone(dto.getPhone() != null ? dto.getPhone() : organization.getPhone()); try { Organization updatedOrganization = repository.save(organization); + // send notification to all members about the update + var memberships = membershipRepository.findByOrganizationId(organization.getId()); + for (var membership : memberships) { + Notification notification = Notification.builder() + .message("The organization " + organization.getOrgName() + " has been updated.") + .link("/organizations/" + organization.getSlug() + "/basic-info") + .recipient(membership.getUser()) + .type(NotificationType.ORGANIZATION_UPDATED) + .build(); + Notification savedNotification = notificationRepository.save(notification); + natsPublisher.publishNotificationEvent(savedNotification.getId()); + } return mapper.toBasicInfoDto(updatedOrganization); } catch (DataIntegrityViolationException ex) { Throwable root = ex.getCause(); From 7c8c6a9d295aaadd9e9c3555e02dea824939c374 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:12:07 +0600 Subject: [PATCH 15/40] fix(notification): set notification type to INVITATION_SENT --- .../heapdog/core/feature/organization/OrganizationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index 888930d..b737544 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -185,7 +185,7 @@ OrganizationSlugCheckResponseDto checkSlugAvailability(String slug) { .message("You have been invited to join the organization: " + organization.getOrgName()) .link("/invitations/accept?code=" + savedInvitation.getCode() + "&org=" + organization.getSlug()) .recipient(user) - .type(NotificationType.INVITATION) + .type(NotificationType.INVITATION_SENT) .build(); notificationRepository.save(notification); From e7b4aba7e21bbedc75966a9eac669c2a25ed3b44 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:12:30 +0600 Subject: [PATCH 16/40] refactor(remove): remove unused code --- .../core/feature/organization/OrganizationService.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index b737544..708acea 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -227,13 +227,6 @@ Page getOrganizationInvitations(String slug, .orElseThrow(() -> new ResourceNotFoundException("Organization", "slug", slug)); Pageable pageable = PageRequest.of(page.intValue() - 1, size.intValue(), Sort.by("createdAt").ascending()); log.info("Getting organization invitations for slug {}", slug); -// var invitations = organizationInvitationRepository.findOrganizationInvitationByOrganizationId(organization.getId(), pageable); -// var i = invitations.stream().map(organizationInvitationMapper::toOrganizationInvitationResponseDto) -// .toList(); -// return OrganizationInvitationsResponseDto.builder() -// .invitations(i) -// .build(); -// return invitations.map(organizationInvitationMapper::toOrganizationInvitationResponseDto); return organizationInvitationRepository.findOrganizationInvitationsByOrganizationId(organization.getId(), pageable); } From f9417f1de2c60a9fdab5999f37c6c0afca75ee35 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:13:07 +0600 Subject: [PATCH 17/40] fix(notification): send notification to the inviter on accepting an invitation --- .../core/feature/organization/OrganizationService.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index 708acea..c189d56 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -279,6 +279,16 @@ OrganizationInvitationAcceptResponseDto acceptInvitation(String slug, user.setCurrentMembership(membership); userRepository.save(user); } + // Create a notification and send it to the creator of the invitation + var creator = invitation.getCreatedBy(); + Notification notification = Notification.builder() + .message(user.getUsername() + " has accepted the invitation to join the organization: " + invitation.getOrganization().getOrgName()) + .link("/organizations/" + invitation.getOrganization().getSlug() + "/members") + .recipient(creator) + .type(NotificationType.INVITATION_ACCEPTED) + .build(); + Notification savedNotification = notificationRepository.save(notification); + natsPublisher.publishNotificationEvent(savedNotification.getId()); return organizationInvitationMapper.toOrganizationInvitationAcceptResponseDto(membership); } From f4b7091761442ae8cfaf6cfbfc298bc4583cc6ff Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:14:04 +0600 Subject: [PATCH 18/40] fix(invitation): prevent leaking invitation info if requested user is not the invitee --- .../core/feature/organization/OrganizationService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index c189d56..639704f 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -298,15 +298,15 @@ public OrganizationInvitationAcceptInfoResponseDto getInvitationAcceptInfo(Strin OrganizationInvitation invitation = organizationInvitationRepository .findByOrganizationSlugAndCode(slug, code) .orElseThrow(() -> new ResourceNotFoundException("Invitation", "code", code)); + if (!invitation.getUser().getId().equals(userId)) { + throw new ResourceNotFoundException("Invitation", "code", code); + } if (invitation.getRevoked()) { throw new IllegalArgumentException("This invitation has been revoked."); } if (invitation.isAccepted()) { throw new IllegalArgumentException("This invitation has already been accepted."); } - if (!invitation.getUser().getId().equals(userId)) { - throw new IllegalArgumentException("This invitation is not for the current user."); - } return organizationInvitationMapper.toOrganizationInvitationAcceptInfoResponseDto(invitation); } From 89212e89c2e7f2630bf76b8dc35acff9ee4ed2c5 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:31:25 +0600 Subject: [PATCH 19/40] feat(type): add INVITATION_SENT, INVITATION_ACCEPTED, ORGANIZATION_MEMBER_ROLE_UPDATED, ORGANIZATION_UPDATED --- .../heapdog/core/feature/notification/NotificationType.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/heapdog/core/feature/notification/NotificationType.java b/src/main/java/io/heapdog/core/feature/notification/NotificationType.java index 5d78240..6766b83 100644 --- a/src/main/java/io/heapdog/core/feature/notification/NotificationType.java +++ b/src/main/java/io/heapdog/core/feature/notification/NotificationType.java @@ -2,6 +2,8 @@ public enum NotificationType { - INVITATION + INVITATION_SENT, + INVITATION_ACCEPTED, + ORGANIZATION_MEMBER_ROLE_UPDATED, ORGANIZATION_UPDATED, } From f02c8d3b7d939c9ec206d0d918c59531faaa64ec Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:31:46 +0600 Subject: [PATCH 20/40] fix(slug): remove slug from dto --- .../organization/OrganizationBasicInfoUpdateRequestDto.java | 2 -- .../heapdog/core/feature/organization/OrganizationService.java | 1 - 2 files changed, 3 deletions(-) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationBasicInfoUpdateRequestDto.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationBasicInfoUpdateRequestDto.java index bed8ee5..2e9e368 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationBasicInfoUpdateRequestDto.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationBasicInfoUpdateRequestDto.java @@ -12,8 +12,6 @@ public class OrganizationBasicInfoUpdateRequestDto { private String name; - private String slug; - @Size(max = 100, message = "Description can be at most 100 characters") private String description; diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index 639704f..4253679 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -118,7 +118,6 @@ OrganizationBasicInfoResponseDto updateOrganization(String slug, OrganizationBas Organization organization = repository.findBySlug(slug) .orElseThrow(() -> new ResourceNotFoundException("Organization", "slug", slug)); organization.setOrgName(dto.getName() != null ? dto.getName() : organization.getOrgName()); - organization.setSlug(dto.getSlug() != null ? dto.getSlug() : organization.getSlug()); organization.setDescription(dto.getDescription() != null ? dto.getDescription() : organization.getDescription()); organization.setEmail(dto.getEmail() != null ? dto.getEmail() : organization.getEmail()); organization.setWebsite(dto.getWebsite() != null ? dto.getWebsite() : organization.getWebsite()); From 716c6a5e5134334ff2da2fd66c04c9374673d697 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:32:15 +0600 Subject: [PATCH 21/40] feat(role): add role update system --- .../organization/OrganizationController.java | 8 ++++++++ .../OrganizationMemberRoleUpdateRequestDto.java | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/main/java/io/heapdog/core/feature/organization/OrganizationMemberRoleUpdateRequestDto.java diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationController.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationController.java index d6a97ac..ff0e7c9 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationController.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationController.java @@ -107,4 +107,12 @@ OrganizationInvitationAcceptResponseDto acceptInvitation(@PathVariable String sl return organizationService.acceptInvitation(slug, dto, user.getId()); } + @PreAuthorize("@organizationSecurity.isAdmin(#slug, authentication)") + @PatchMapping("/{slug}/membership/{membershipId}/role") + OrganizationMemberResponseDto updateOrganizationMemberRole(@PathVariable String slug, + @PathVariable Long membershipId, + @Valid @RequestBody OrganizationMemberRoleUpdateRequestDto dto) { + return organizationService.updateOrganizationMemberRole(slug, membershipId, dto); + } + } diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationMemberRoleUpdateRequestDto.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationMemberRoleUpdateRequestDto.java new file mode 100644 index 0000000..d64eca0 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationMemberRoleUpdateRequestDto.java @@ -0,0 +1,13 @@ +package io.heapdog.core.feature.organization; + + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class OrganizationMemberRoleUpdateRequestDto { + + private OrganizationRole role; + +} From 229b44ba4e57c0134dcd85c86d84ab0ff4e8bf74 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:32:36 +0600 Subject: [PATCH 22/40] fix(field): include membershipId --- .../core/feature/organization/OrganizationMemberResponseDto.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationMemberResponseDto.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationMemberResponseDto.java index 16a8c36..db540a9 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationMemberResponseDto.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationMemberResponseDto.java @@ -10,4 +10,5 @@ public class OrganizationMemberResponseDto { private String username; private String email; private OrganizationRole role; + private Long membershipId; } From b1aef3c659e12c1ca80d673aa1f33f67fe178d17 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:34:29 +0600 Subject: [PATCH 23/40] feat(route): add route to fetch service user by id --- .../core/feature/serviceuser/ServiceUserController.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserController.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserController.java index 3ddeb11..d929dc7 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserController.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserController.java @@ -14,6 +14,12 @@ public class ServiceUserController { private final ServiceUserService serviceUserService; + @GetMapping("/{id}") + ResponseEntity getServiceUser(@PathVariable Long id) { + var resp = serviceUserService.getServiceUser(id); + return ResponseEntity.ok(resp); + } + @PostMapping ResponseEntity createServiceUser(@Valid @RequestBody ServiceUserCreateRequestDto request) { From 45f8e0055a1d1655d8530cff182c83947fd4cbbf Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:34:50 +0600 Subject: [PATCH 24/40] feat(route): add route to fetch available permissions for service user --- .../core/feature/serviceuser/ServiceUserController.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserController.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserController.java index d929dc7..6537473 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserController.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserController.java @@ -27,4 +27,10 @@ ResponseEntity getServiceUser(@PathVariable Long id) return ResponseEntity.ok(resp); } + @GetMapping("/permissions") + ResponseEntity getAvailablePermissions() { + var resp = serviceUserService.getServiceUserPermissions(); + return ResponseEntity.ok(resp); + } + } From d5206f810065cbd689a695227a695e6aed8f6642 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:35:18 +0600 Subject: [PATCH 25/40] fix(dto): handle permission while creating/reading a service user --- .../core/feature/serviceuser/ServiceUserCreateRequestDto.java | 3 +++ .../feature/serviceuser/ServiceUserCreateResponseDto.java | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserCreateRequestDto.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserCreateRequestDto.java index 5dc3038..5a4da17 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserCreateRequestDto.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserCreateRequestDto.java @@ -4,11 +4,14 @@ import lombok.Builder; import lombok.Data; +import java.util.Set; + @Data @Builder public class ServiceUserCreateRequestDto { @NotEmpty private String name; + Set permissions; } diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserCreateResponseDto.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserCreateResponseDto.java index 1ef85bd..c1e9049 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserCreateResponseDto.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserCreateResponseDto.java @@ -4,12 +4,14 @@ import lombok.Builder; import lombok.Data; +import java.util.Set; + @Data @Builder public class ServiceUserCreateResponseDto { - private Long id; private String name; private String apiKey; private Boolean enabled; + private Set permissions; } From 86b97ae30b23cae00640fd7369287bca72449f5d Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:25:08 +0600 Subject: [PATCH 26/40] feat(permission): add new permission READ_SSE_TOKEN --- .../heapdog/core/feature/serviceuser/ServiceUserPermission.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java index 386eabe..db717d9 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java @@ -5,6 +5,8 @@ public enum ServiceUserPermission { READ_HEAPDOG_USER("read:heapdog_user"), WRITE_HEAPDOG_USER("write:heapdog_user"), READ_NOTIFICATION("read:notification"); + READ_NOTIFICATION("read:notification"), + READ_SSE_TOKEN("read:sse_token"); private final String permission; From ec10d8312fa112380252719e130b938a43fd4bd6 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:26:10 +0600 Subject: [PATCH 27/40] feat(label): introduce permission label --- .../core/feature/serviceuser/ServiceUserPermission.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java index db717d9..b78358f 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java @@ -4,14 +4,17 @@ public enum ServiceUserPermission { READ_HEAPDOG_USER("read:heapdog_user"), WRITE_HEAPDOG_USER("write:heapdog_user"), - READ_NOTIFICATION("read:notification"); READ_NOTIFICATION("read:notification"), READ_SSE_TOKEN("read:sse_token"); + private final String label; + private final String permission; ServiceUserPermission(String permission) { this.permission = permission; + public String getLabel() { + return label; } public String getPermission() { From bcfd828853672586ececf0954889b69d5c11bf95 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:26:24 +0600 Subject: [PATCH 28/40] feat(description): add meaningful description --- .../serviceuser/ServiceUserPermission.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java index b78358f..8d4075f 100644 --- a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermission.java @@ -8,16 +8,22 @@ public enum ServiceUserPermission { READ_SSE_TOKEN("read:sse_token"); private final String label; + private final String description = switch (this) { + case READ_HEAPDOG_USER -> "Permission to read Heapdog user data."; + case WRITE_HEAPDOG_USER -> "Permission to modify Heapdog user data."; + case READ_NOTIFICATION -> "Permission to read notifications."; + case READ_SSE_TOKEN -> "Permission to read SSE tokens."; + }; - private final String permission; + ServiceUserPermission(String description) { + this.label = description; + } - ServiceUserPermission(String permission) { - this.permission = permission; public String getLabel() { return label; } - public String getPermission() { - return permission; + public String getDescription() { + return description; } } From 6e25eeda114a30640537c805629f21bf9acd16f6 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:36:31 +0600 Subject: [PATCH 29/40] feat(dto): add dto to handle service user read response --- .../ServiceUserReadResponseDto.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserReadResponseDto.java diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserReadResponseDto.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserReadResponseDto.java new file mode 100644 index 0000000..3cd387a --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserReadResponseDto.java @@ -0,0 +1,31 @@ +package io.heapdog.core.feature.serviceuser; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.time.Instant; +import java.util.Set; + +@Builder +@Data +@AllArgsConstructor +public class ServiceUserReadResponseDto { + private Long id; + private String name; + private String apiKey; + private Boolean enabled; + private Instant createdAt; + private Instant updatedAt; + private User createdBy; + private Set permissions; + private Instant lastAccessedAt; + + @Data + @Builder + public static class User { + private Long id; + private String username; + } +} From 803eecd23041c389734b2eccbfb698569f2cec41 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:37:02 +0600 Subject: [PATCH 30/40] feat(dto): add dto to handle service user permissions response --- .../ServiceUserPermissionResponseDto.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermissionResponseDto.java diff --git a/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermissionResponseDto.java b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermissionResponseDto.java new file mode 100644 index 0000000..5b794d1 --- /dev/null +++ b/src/main/java/io/heapdog/core/feature/serviceuser/ServiceUserPermissionResponseDto.java @@ -0,0 +1,22 @@ +package io.heapdog.core.feature.serviceuser; + + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Builder +@Data +public class ServiceUserPermissionResponseDto { + + private List permissions; + + @Builder + @Data + static class Permission { + private String name; + private String label; + private String description; + } +} From fb73957c7b23665f19bbaa34d9c323d357a6bc7c Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:41:36 +0600 Subject: [PATCH 31/40] feat(nats): add nats-dev service --- compose.dev.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/compose.dev.yml b/compose.dev.yml index 5035f87..e2f7d0f 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -39,6 +39,15 @@ services: networks: - heapdog-network-dev + nats-dev: + container_name: nats-dev + image: nats:2.12.2-alpine3.22 + ports: + - "4223:4222" + - "8223:8222" + networks: + - heapdog-network-dev + volumes: db_data_dev: From 4e6d62b3eae9eace08c9c948a67a095a82891d85 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:41:53 +0600 Subject: [PATCH 32/40] feat(nats): add nats service --- compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/compose.yml b/compose.yml index 4340344..7dab70e 100644 --- a/compose.yml +++ b/compose.yml @@ -63,6 +63,14 @@ services: networks: - heapdog-network + nats: + container_name: nats-dev + image: nats:2.12.2-alpine3.22 + ports: + - "4222:4222" + - "8222:8222" + networks: + - heapdog-network test: build: From 328797352ad97e85e56381eab98ae29bab8acc6b Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:39:12 +0600 Subject: [PATCH 33/40] feat(nats): set nats config --- src/main/resources/application-dev.yml | 6 +++++- src/main/resources/application-prod.yml | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 8e571e0..6527cf9 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -27,4 +27,8 @@ logging: server: servlet: context-path: /api/v1 - port: ${SERVER_PORT:8080} \ No newline at end of file + port: ${SERVER_PORT:8080} + +nats: + url: ${NATS_URL:nats://localhost:4222} + notificationSubject: ${NATS_NOTIFICATION_SUBJECT:notifications} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index c957d04..2ed20a6 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -24,3 +24,7 @@ server: servlet: context-path: /api/v1 port: ${SERVER_PORT:8080} + +nats: + url: ${NATS_URL:nats://localhost:4222} + notificationSubject: ${NATS_NOTIFICATION_SUBJECT:notifications} From f9a32d6e3e9851dcc56cc4c6a0caded141805cbd Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 00:39:31 +0600 Subject: [PATCH 34/40] feat(nats): add nats dependency --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index a2b5c42..2589a42 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.mapstruct:mapstruct:1.6.3' implementation "org.projectlombok:lombok-mapstruct-binding:0.2.0" implementation 'org.flywaydb:flyway-database-postgresql' + implementation 'io.nats:jnats:2.24.1' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' From 977a966f9c71925bc7bb3b3f0f86c5743d891925 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:02:58 +0600 Subject: [PATCH 35/40] feat(nats): add configuration and prepare Connection bean --- .../io/heapdog/core/config/NatsConfig.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/io/heapdog/core/config/NatsConfig.java diff --git a/src/main/java/io/heapdog/core/config/NatsConfig.java b/src/main/java/io/heapdog/core/config/NatsConfig.java new file mode 100644 index 0000000..fc4a4d6 --- /dev/null +++ b/src/main/java/io/heapdog/core/config/NatsConfig.java @@ -0,0 +1,23 @@ +package io.heapdog.core.config; + + +import io.nats.client.Connection; +import io.nats.client.Nats; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +@Configuration +public class NatsConfig { + + @Value("${nats.url}") + private String natsUrl; + + @Bean(destroyMethod = "close") + public Connection natsConnection() throws IOException, InterruptedException { + return Nats.connect(natsUrl); + } + +} From 5acfe64658ebebbf8e4628d513d3bb778324e637 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:03:26 +0600 Subject: [PATCH 36/40] feat(nats): add nats publisher --- .../io/heapdog/core/shared/NatsPublisher.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/io/heapdog/core/shared/NatsPublisher.java diff --git a/src/main/java/io/heapdog/core/shared/NatsPublisher.java b/src/main/java/io/heapdog/core/shared/NatsPublisher.java new file mode 100644 index 0000000..3f6ed8f --- /dev/null +++ b/src/main/java/io/heapdog/core/shared/NatsPublisher.java @@ -0,0 +1,31 @@ +package io.heapdog.core.shared; + + +import io.nats.client.Connection; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; + +@Service +@RequiredArgsConstructor +@Slf4j +public class NatsPublisher { + + private final Connection natsConnection; + + @Value("${nats.notificationSubject}") + private String NOTIFICATION_SUBJECT; + + + private void publish(String subject, String message) { + natsConnection.publish(subject, message.getBytes(StandardCharsets.UTF_8)); + } + + public void publishNotificationEvent(Long notificationId) { + log.info("Publishing notification event for ID: {} in subject: {}", notificationId, NOTIFICATION_SUBJECT); + publish(NOTIFICATION_SUBJECT, notificationId.toString()); + } +} From f67b04e49c5377aed2e8bed2fdc2519343fc6823 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:05:47 +0600 Subject: [PATCH 37/40] feat(nats): publish notification id in nats --- .../core/feature/organization/OrganizationService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java index 4253679..10f88f1 100644 --- a/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java +++ b/src/main/java/io/heapdog/core/feature/organization/OrganizationService.java @@ -8,6 +8,7 @@ import io.heapdog.core.feature.user.HeapDogUserRepository; import io.heapdog.core.shared.ConstraintToFieldMapper; import io.heapdog.core.shared.DuplicateResourceException; +import io.heapdog.core.shared.NatsPublisher; import io.heapdog.core.shared.ResourceNotFoundException; import io.heapdog.core.shared.util.OtpGenerator; import jakarta.transaction.Transactional; @@ -38,6 +39,7 @@ public class OrganizationService { private final OrganizationInvitationMapper organizationInvitationMapper; private final NotificationRepository notificationRepository; + private final NatsPublisher natsPublisher; OrganizationBasicInfoResponseDto getBasicInfo(String slug) { Organization organization = repository.findBySlug(slug) @@ -186,7 +188,8 @@ OrganizationSlugCheckResponseDto checkSlugAvailability(String slug) { .recipient(user) .type(NotificationType.INVITATION_SENT) .build(); - notificationRepository.save(notification); + Notification saved = notificationRepository.save(notification); + natsPublisher.publishNotificationEvent(saved.getId()); return organizationInvitationMapper.toDto(savedInvitation); } From db388fd89dccefd494c7ebc274faacc5d1f63531 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:44:54 +0600 Subject: [PATCH 38/40] feat(nats): add nats config in test profile --- src/main/resources/application-test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 6d1410d..871cd74 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -23,4 +23,8 @@ jwt: logging: level: - org.springframework.security: trace \ No newline at end of file + org.springframework.security: trace + +nats: + url: ${NATS_URL:nats://localhost:4222} + notificationSubject: ${NATS_NOTIFICATION_SUBJECT:notifications} \ No newline at end of file From 416ae009626b7787f95dae1c36c7becc26198657 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:45:16 +0600 Subject: [PATCH 39/40] feat(nats): add nats-dev to depends_on of backend --- compose.dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/compose.dev.yml b/compose.dev.yml index e2f7d0f..055b859 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -16,6 +16,7 @@ services: - "${SERVER_PORT:-9090}:${SERVER_PORT:-9090}" depends_on: - db-dev + - nats-dev healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:${SERVER_PORT:-9090}/actuator/health"] interval: 10s From 7f60a6194acc39026e29c1bf1dc6003f96c40f06 Mon Sep 17 00:00:00 2001 From: parthokr Date: Sat, 13 Dec 2025 01:44:24 +0600 Subject: [PATCH 40/40] fix(test): remove test --- .github/workflows/ci.yml | 44 ---------------------------------------- compose.yml | 14 +------------ 2 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index e99e435..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: CI - -on: - push: - branches: [ "main", "dev" ] - pull_request: - branches: [ "main", "dev" ] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Create dummy .env file - run: touch .env - - - name: Build services - run: docker compose build test - - - name: Run tests with coverage - run: docker compose up --abort-on-container-exit --exit-code-from test test - - - name: Show coverage summary - run: | - COVERED=$(grep -o 'covered="[0-9]*"' build/reports/jacoco/test/jacocoTestReport.xml | awk -F\" '{sum+=$2} END {print sum}') - MISSED=$(grep -o 'missed="[0-9]*"' build/reports/jacoco/test/jacocoTestReport.xml | awk -F\" '{sum+=$2} END {print sum}') - TOTAL=$((COVERED + MISSED)) - PERCENT=$((100 * COVERED / TOTAL)) - echo "==== COVERAGE SUMMARY ====" - echo "Lines covered: $COVERED / $TOTAL ($PERCENT%)" - echo "==========================" - - - name: Upload coverage report - uses: actions/upload-artifact@v4 - with: - name: jacoco-report - path: build/reports/jacoco/test - diff --git a/compose.yml b/compose.yml index 7dab70e..1b2dccb 100644 --- a/compose.yml +++ b/compose.yml @@ -64,7 +64,7 @@ services: - heapdog-network nats: - container_name: nats-dev + container_name: nats image: nats:2.12.2-alpine3.22 ports: - "4222:4222" @@ -72,18 +72,6 @@ services: networks: - heapdog-network - test: - build: - context: . - dockerfile: Dockerfile.test - working_dir: /home/gradle/app - environment: - SPRING_PROFILES_ACTIVE: test - volumes: - - .:/home/gradle/app - - ./build/reports:/home/gradle/app/build/reports - command: [ "./gradlew", "test", "jacocoTestReport", "--info" ] - volumes: db_data_prod: jetbrains_cache: