From c41c4ecf2047bd96ff380ee45529618ef2f10910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hor=C3=A1nszki=20Patrik?= Date: Tue, 14 Apr 2026 23:38:16 +0200 Subject: [PATCH] EX-368: Implement auditlogs with Javers --- backend/exence/build.gradle.kts | 2 + backend/exence/gradle/libs.versions.toml | 2 + .../finance/common/dto/SliceResponse.java | 19 +++ .../common/entity/BaseAuditableEntity.java | 5 + .../common/entity/BaseWorkspaceEntity.java | 2 + .../finance/common/util/ResponseFactory.java | 6 + .../exence/finance/config/JaversConfig.java | 40 ++++++ .../controller/AdminAuditLogController.java | 12 ++ .../controller/AuditLogController.java | 12 ++ .../impl/AdminAuditLogControllerImpl.java | 38 ++++++ .../impl/AuditLogControllerImpl.java | 38 ++++++ .../auditlog/dto/AuditLogChangeDTO.java | 3 + .../modules/auditlog/dto/AuditLogDTO.java | 13 ++ .../modules/auditlog/dto/AuditLogFilter.java | 8 ++ .../auditlog/enums/AuditableEntityType.java | 27 ++++ .../modules/auditlog/enums/ChangeType.java | 7 + .../auditlog/mapper/AuditLogMapper.java | 67 ++++++++++ .../auditlog/service/AuditLogService.java | 13 ++ .../service/impl/AuditLogServiceImpl.java | 120 ++++++++++++++++++ .../auth/service/impl/AuthServiceImpl.java | 26 +--- .../modules/category/entity/Category.java | 2 + .../repository/CategoryRepository.java | 2 + .../service/impl/CategoryServiceImpl.java | 2 +- .../debt/repository/DebtRepository.java | 2 + .../goal/repository/GoalRepository.java | 2 + .../modules/investment/entity/Investment.java | 6 +- .../repository/InvestmentRepository.java | 2 + .../statistics/service/WidgetService.java | 3 + .../service/impl/WidgetServiceImpl.java | 28 +++- .../RecurringTransactionRepository.java | 2 + .../repository/TransactionRepository.java | 2 + .../modules/workspace/entity/Workspace.java | 3 + .../workspace/entity/WorkspaceMember.java | 4 + .../repository/WorkspaceMemberRepository.java | 2 + .../repository/WorkspaceRepository.java | 2 + .../service/impl/WorkspaceServiceImpl.java | 3 + .../exence/src/main/resources/application.yml | 3 + 37 files changed, 499 insertions(+), 31 deletions(-) create mode 100644 backend/exence/src/main/java/com/exence/finance/common/dto/SliceResponse.java create mode 100644 backend/exence/src/main/java/com/exence/finance/config/JaversConfig.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/AdminAuditLogController.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/AuditLogController.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/impl/AdminAuditLogControllerImpl.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/impl/AuditLogControllerImpl.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogChangeDTO.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogDTO.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogFilter.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/enums/AuditableEntityType.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/enums/ChangeType.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/mapper/AuditLogMapper.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/service/AuditLogService.java create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/auditlog/service/impl/AuditLogServiceImpl.java diff --git a/backend/exence/build.gradle.kts b/backend/exence/build.gradle.kts index d1bbae4a..a1e7398a 100644 --- a/backend/exence/build.gradle.kts +++ b/backend/exence/build.gradle.kts @@ -73,6 +73,8 @@ dependencies { implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.mail) implementation(libs.spring.boot.starter.validation) + // Audit logging + implementation(libs.javers.spring.boot.starter.sql) // JWT implementation(libs.jjwt.api) diff --git a/backend/exence/gradle/libs.versions.toml b/backend/exence/gradle/libs.versions.toml index 85169b76..e1f1e93b 100644 --- a/backend/exence/gradle/libs.versions.toml +++ b/backend/exence/gradle/libs.versions.toml @@ -15,6 +15,7 @@ mockito = "5.15.2" querydsl = "5.1.0" argon2-jvm = "2.11" jacoco = "0.8.12" +javers = "7.9.0" [libraries] spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" } @@ -37,6 +38,7 @@ mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } querydsl-jpa = { module = "com.querydsl:querydsl-jpa", version.ref = "querydsl" } querydsl-apt = { module = "com.querydsl:querydsl-apt", version.ref = "querydsl" } argon2-jvm = { module = "de.mkammerer:argon2-jvm", version.ref = "argon2-jvm" } +javers-spring-boot-starter-sql = { module = "org.javers:javers-spring-boot-starter-sql", version.ref = "javers" } [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } diff --git a/backend/exence/src/main/java/com/exence/finance/common/dto/SliceResponse.java b/backend/exence/src/main/java/com/exence/finance/common/dto/SliceResponse.java new file mode 100644 index 00000000..423513fa --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/common/dto/SliceResponse.java @@ -0,0 +1,19 @@ +package com.exence.finance.common.dto; + +import java.util.List; +import org.springframework.data.domain.Slice; + +public record SliceResponse( + List content, int page, int size, boolean first, boolean last, boolean hasNext, int numberOfElements) { + + public static SliceResponse from(Slice slice) { + return new SliceResponse<>( + slice.getContent(), + slice.getNumber(), + slice.getSize(), + slice.isFirst(), + slice.isLast(), + slice.hasNext(), + slice.getNumberOfElements()); + } +} diff --git a/backend/exence/src/main/java/com/exence/finance/common/entity/BaseAuditableEntity.java b/backend/exence/src/main/java/com/exence/finance/common/entity/BaseAuditableEntity.java index c44a3d76..ef5f4b07 100644 --- a/backend/exence/src/main/java/com/exence/finance/common/entity/BaseAuditableEntity.java +++ b/backend/exence/src/main/java/com/exence/finance/common/entity/BaseAuditableEntity.java @@ -11,6 +11,7 @@ import lombok.experimental.SuperBuilder; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import org.javers.core.metamodel.annotation.DiffIgnore; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -24,18 +25,22 @@ @MappedSuperclass public abstract class BaseAuditableEntity { + @DiffIgnore @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; + @DiffIgnore @UpdateTimestamp @Column(name = "updated_at") private Instant updatedAt; + @DiffIgnore @CreatedBy @Column(name = "created_by", nullable = false, updatable = false) private String createdBy; + @DiffIgnore @LastModifiedBy @Column(name = "updated_by") private String updatedBy; diff --git a/backend/exence/src/main/java/com/exence/finance/common/entity/BaseWorkspaceEntity.java b/backend/exence/src/main/java/com/exence/finance/common/entity/BaseWorkspaceEntity.java index 362a6ecf..8e27542a 100644 --- a/backend/exence/src/main/java/com/exence/finance/common/entity/BaseWorkspaceEntity.java +++ b/backend/exence/src/main/java/com/exence/finance/common/entity/BaseWorkspaceEntity.java @@ -10,6 +10,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.SuperBuilder; +import org.javers.core.metamodel.annotation.DiffIgnore; @Getter @Setter @@ -19,6 +20,7 @@ @MappedSuperclass public abstract class BaseWorkspaceEntity extends BaseAuditableEntity { + @DiffIgnore @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "workspace_id", nullable = false) private Workspace workspace; diff --git a/backend/exence/src/main/java/com/exence/finance/common/util/ResponseFactory.java b/backend/exence/src/main/java/com/exence/finance/common/util/ResponseFactory.java index 09ca0b57..1a742689 100644 --- a/backend/exence/src/main/java/com/exence/finance/common/util/ResponseFactory.java +++ b/backend/exence/src/main/java/com/exence/finance/common/util/ResponseFactory.java @@ -1,9 +1,11 @@ package com.exence.finance.common.util; import com.exence.finance.common.dto.PageResponse; +import com.exence.finance.common.dto.SliceResponse; import java.net.URI; import lombok.experimental.UtilityClass; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; @@ -36,6 +38,10 @@ public static ResponseEntity> page(Page page) { return ResponseEntity.ok(PageResponse.from(page)); } + public static ResponseEntity> slice(Slice slice) { + return ResponseEntity.ok(SliceResponse.from(slice)); + } + public static ResponseEntity okWithCookies(T body, ResponseCookie... cookies) { var builder = ResponseEntity.ok(); for (ResponseCookie cookie : cookies) { diff --git a/backend/exence/src/main/java/com/exence/finance/config/JaversConfig.java b/backend/exence/src/main/java/com/exence/finance/config/JaversConfig.java new file mode 100644 index 00000000..891fce4f --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/config/JaversConfig.java @@ -0,0 +1,40 @@ +package com.exence.finance.config; + +import com.exence.finance.modules.workspace.context.WorkspaceContextHolder; +import java.util.Map; +import org.javers.spring.auditable.AuthorProvider; +import org.javers.spring.auditable.CommitPropertiesProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +@Configuration +public class JaversConfig { + + @Bean + public AuthorProvider authorProvider() { + return () -> { + var auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) { + return auth.getName(); + } + return "SYSTEM"; + }; + } + + @Bean + public CommitPropertiesProvider commitPropertiesProvider() { + return new CommitPropertiesProvider() { + @Override + public Map provideForCommittedObject(Object domainObject) { + if (WorkspaceContextHolder.isSet()) { + return Map.of( + "workspaceId", + WorkspaceContextHolder.getWorkspaceId().toString()); + } + return Map.of(); + } + }; + } +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/AdminAuditLogController.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/AdminAuditLogController.java new file mode 100644 index 00000000..237c7e0a --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/AdminAuditLogController.java @@ -0,0 +1,12 @@ +package com.exence.finance.modules.auditlog.controller; + +import com.exence.finance.common.dto.SliceResponse; +import com.exence.finance.modules.auditlog.dto.AuditLogDTO; +import com.exence.finance.modules.auditlog.dto.AuditLogFilter; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; + +public interface AdminAuditLogController { + + ResponseEntity> getAllAuditLogs(AuditLogFilter filter, Pageable pageable); +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/AuditLogController.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/AuditLogController.java new file mode 100644 index 00000000..9af2d337 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/AuditLogController.java @@ -0,0 +1,12 @@ +package com.exence.finance.modules.auditlog.controller; + +import com.exence.finance.common.dto.SliceResponse; +import com.exence.finance.modules.auditlog.dto.AuditLogDTO; +import com.exence.finance.modules.auditlog.dto.AuditLogFilter; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; + +public interface AuditLogController { + + ResponseEntity> getWorkspaceAuditLogs(AuditLogFilter filter, Pageable pageable); +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/impl/AdminAuditLogControllerImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/impl/AdminAuditLogControllerImpl.java new file mode 100644 index 00000000..b158c120 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/impl/AdminAuditLogControllerImpl.java @@ -0,0 +1,38 @@ +package com.exence.finance.modules.auditlog.controller.impl; + +import static com.exence.finance.common.util.ApplicationConstants.DEFAULT_PAGE_SIZE; + +import com.exence.finance.common.dto.SliceResponse; +import com.exence.finance.common.util.ResponseFactory; +import com.exence.finance.modules.auditlog.controller.AdminAuditLogController; +import com.exence.finance.modules.auditlog.dto.AuditLogDTO; +import com.exence.finance.modules.auditlog.dto.AuditLogFilter; +import com.exence.finance.modules.auditlog.service.AuditLogService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/admin/audit-logs") +@CrossOrigin(origins = "http://localhost:4200") +@RequiredArgsConstructor +public class AdminAuditLogControllerImpl implements AdminAuditLogController { + + private final AuditLogService auditLogService; + + @Override + @GetMapping + public ResponseEntity> getAllAuditLogs( + @ModelAttribute AuditLogFilter filter, + @PageableDefault(size = DEFAULT_PAGE_SIZE, sort = "changedAt", direction = Sort.Direction.DESC) + Pageable pageable) { + return ResponseFactory.slice(auditLogService.getAllAuditLogs(filter, pageable)); + } +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/impl/AuditLogControllerImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/impl/AuditLogControllerImpl.java new file mode 100644 index 00000000..e31e7964 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/controller/impl/AuditLogControllerImpl.java @@ -0,0 +1,38 @@ +package com.exence.finance.modules.auditlog.controller.impl; + +import static com.exence.finance.common.util.ApplicationConstants.DEFAULT_PAGE_SIZE; + +import com.exence.finance.common.dto.SliceResponse; +import com.exence.finance.common.util.ResponseFactory; +import com.exence.finance.modules.auditlog.controller.AuditLogController; +import com.exence.finance.modules.auditlog.dto.AuditLogDTO; +import com.exence.finance.modules.auditlog.dto.AuditLogFilter; +import com.exence.finance.modules.auditlog.service.AuditLogService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/audit-logs") +@CrossOrigin(origins = "http://localhost:4200") +@RequiredArgsConstructor +public class AuditLogControllerImpl implements AuditLogController { + + private final AuditLogService auditLogService; + + @Override + @GetMapping() + public ResponseEntity> getWorkspaceAuditLogs( + @ModelAttribute AuditLogFilter filter, + @PageableDefault(size = DEFAULT_PAGE_SIZE, sort = "changedAt", direction = Sort.Direction.DESC) + Pageable pageable) { + return ResponseFactory.slice(auditLogService.getWorkspaceAuditLogs(filter, pageable)); + } +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogChangeDTO.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogChangeDTO.java new file mode 100644 index 00000000..9e20a625 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogChangeDTO.java @@ -0,0 +1,3 @@ +package com.exence.finance.modules.auditlog.dto; + +public record AuditLogChangeDTO(String field, String from, String to) {} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogDTO.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogDTO.java new file mode 100644 index 00000000..2fca3ae5 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogDTO.java @@ -0,0 +1,13 @@ +package com.exence.finance.modules.auditlog.dto; + +import com.exence.finance.modules.auditlog.enums.ChangeType; +import java.time.Instant; +import java.util.List; + +public record AuditLogDTO( + String entityType, + String entityId, + ChangeType action, + Instant changedAt, + String changedBy, + List changes) {} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogFilter.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogFilter.java new file mode 100644 index 00000000..0a8db036 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/dto/AuditLogFilter.java @@ -0,0 +1,8 @@ +package com.exence.finance.modules.auditlog.dto; + +import com.exence.finance.modules.auditlog.enums.AuditableEntityType; +import com.exence.finance.modules.auditlog.enums.ChangeType; +import java.time.LocalDate; + +public record AuditLogFilter( + AuditableEntityType entityType, LocalDate from, LocalDate to, ChangeType changeType, String changedBy) {} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/enums/AuditableEntityType.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/enums/AuditableEntityType.java new file mode 100644 index 00000000..3ff3a2da --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/enums/AuditableEntityType.java @@ -0,0 +1,27 @@ +package com.exence.finance.modules.auditlog.enums; + +import com.exence.finance.modules.category.entity.Category; +import com.exence.finance.modules.debt.entity.Debt; +import com.exence.finance.modules.goal.entity.Goal; +import com.exence.finance.modules.investment.entity.Investment; +import com.exence.finance.modules.transaction.entity.RecurringTransaction; +import com.exence.finance.modules.transaction.entity.Transaction; +import com.exence.finance.modules.workspace.entity.Workspace; +import com.exence.finance.modules.workspace.entity.WorkspaceMember; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AuditableEntityType { + TRANSACTION(Transaction.class), + CATEGORY(Category.class), + GOAL(Goal.class), + DEBT(Debt.class), + INVESTMENT(Investment.class), + RECURRING_TRANSACTION(RecurringTransaction.class), + WORKSPACE(Workspace.class), + WORKSPACE_MEMBER(WorkspaceMember.class); + + private final Class entityClass; +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/enums/ChangeType.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/enums/ChangeType.java new file mode 100644 index 00000000..b70954a0 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/enums/ChangeType.java @@ -0,0 +1,7 @@ +package com.exence.finance.modules.auditlog.enums; + +public enum ChangeType { + CREATED, + UPDATED, + DELETED +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/mapper/AuditLogMapper.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/mapper/AuditLogMapper.java new file mode 100644 index 00000000..3d7d2ea3 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/mapper/AuditLogMapper.java @@ -0,0 +1,67 @@ +package com.exence.finance.modules.auditlog.mapper; + +import com.exence.finance.modules.auditlog.dto.AuditLogChangeDTO; +import com.exence.finance.modules.auditlog.dto.AuditLogDTO; +import com.exence.finance.modules.auditlog.enums.ChangeType; +import java.time.ZoneOffset; +import java.util.List; +import java.util.stream.Collectors; +import org.javers.core.diff.Change; +import org.javers.core.diff.changetype.NewObject; +import org.javers.core.diff.changetype.ObjectRemoved; +import org.javers.core.diff.changetype.ValueChange; +import org.javers.core.metamodel.object.GlobalId; +import org.springframework.stereotype.Component; + +@Component +public class AuditLogMapper { + + public AuditLogDTO toAuditLogDTO(List group) { + Change first = group.get(0); + var commitMeta = first.getCommitMetadata().get(); + GlobalId globalId = first.getAffectedGlobalId(); + + String entityType = extractEntityType(globalId); + String entityId = extractEntityId(globalId); + String changedBy = commitMeta.getAuthor(); + var changedAt = commitMeta.getCommitDate().toInstant(ZoneOffset.UTC); + + boolean isCreated = group.stream().anyMatch(c -> c instanceof NewObject); + boolean isDeleted = group.stream().anyMatch(c -> c instanceof ObjectRemoved); + + ChangeType action; + List fieldChanges; + + if (isCreated) { + action = ChangeType.CREATED; + fieldChanges = List.of(); + } else if (isDeleted) { + action = ChangeType.DELETED; + fieldChanges = List.of(); + } else { + action = ChangeType.UPDATED; + fieldChanges = group.stream() + .filter(c -> c instanceof ValueChange) + .map(c -> (ValueChange) c) + .map(vc -> new AuditLogChangeDTO( + vc.getPropertyName(), + vc.getLeft() != null ? vc.getLeft().toString() : null, + vc.getRight() != null ? vc.getRight().toString() : null)) + .collect(Collectors.toList()); + } + + return new AuditLogDTO(entityType, entityId, action, changedAt, changedBy, fieldChanges); + } + + private String extractEntityType(GlobalId globalId) { + String typeName = globalId.getTypeName(); + int lastDot = typeName.lastIndexOf('.'); + return lastDot >= 0 ? typeName.substring(lastDot + 1) : typeName; + } + + private String extractEntityId(GlobalId globalId) { + String raw = globalId.toString(); + int slash = raw.lastIndexOf('/'); + return slash >= 0 ? raw.substring(slash + 1) : raw; + } +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/service/AuditLogService.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/service/AuditLogService.java new file mode 100644 index 00000000..40156d74 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/service/AuditLogService.java @@ -0,0 +1,13 @@ +package com.exence.finance.modules.auditlog.service; + +import com.exence.finance.modules.auditlog.dto.AuditLogDTO; +import com.exence.finance.modules.auditlog.dto.AuditLogFilter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface AuditLogService { + + Slice getWorkspaceAuditLogs(AuditLogFilter filter, Pageable pageable); + + Slice getAllAuditLogs(AuditLogFilter filter, Pageable pageable); +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auditlog/service/impl/AuditLogServiceImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/service/impl/AuditLogServiceImpl.java new file mode 100644 index 00000000..1e0a092b --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/auditlog/service/impl/AuditLogServiceImpl.java @@ -0,0 +1,120 @@ +package com.exence.finance.modules.auditlog.service.impl; + +import com.exence.finance.common.annotations.transaction.ReadTransactional; +import com.exence.finance.common.exception.ErrorCode; +import com.exence.finance.common.exception.ExenceException; +import com.exence.finance.modules.auditlog.dto.AuditLogDTO; +import com.exence.finance.modules.auditlog.dto.AuditLogFilter; +import com.exence.finance.modules.auditlog.enums.ChangeType; +import com.exence.finance.modules.auditlog.mapper.AuditLogMapper; +import com.exence.finance.modules.auditlog.service.AuditLogService; +import com.exence.finance.modules.auth.service.UserService; +import com.exence.finance.modules.workspace.context.WorkspaceContextHolder; +import com.exence.finance.modules.workspace.entity.WorkspaceMember; +import com.exence.finance.modules.workspace.enums.WorkspaceRole; +import com.exence.finance.modules.workspace.service.WorkspaceMembershipService; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.javers.core.Changes; +import org.javers.core.Javers; +import org.javers.core.metamodel.object.SnapshotType; +import org.javers.repository.jql.QueryBuilder; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuditLogServiceImpl implements AuditLogService { + + private final Javers javers; + private final UserService userService; + private final WorkspaceMembershipService workspaceMembershipService; + private final AuditLogMapper auditLogMapper; + + @Override + @ReadTransactional + public Slice getWorkspaceAuditLogs(AuditLogFilter filter, Pageable pageable) { + Long userId = userService.getCurrentUserId(); + Long workspaceId = WorkspaceContextHolder.getWorkspaceId(); + WorkspaceMember member = workspaceMembershipService.getWorkspaceMember(workspaceId, userId); + if (member.getRole() != WorkspaceRole.OWNER) { + throw new ExenceException(ErrorCode.WORKSPACE_OWNER_REQUIRED); + } + return querySlice(filter, workspaceId, pageable); + } + + @Override + @ReadTransactional + public Slice getAllAuditLogs(AuditLogFilter filter, Pageable pageable) { + return querySlice(filter, null, pageable); + } + + private Slice querySlice(AuditLogFilter filter, Long workspaceId, Pageable pageable) { + QueryBuilder qb = buildQueryBuilder(filter, workspaceId) + .limit(pageable.getPageSize() + 1) + .skip((int) pageable.getOffset()); + + Changes changes = javers.findChanges(qb.build()); + + List all = changes.stream() + .filter(c -> c.getCommitMetadata().isPresent()) + .collect(Collectors.groupingBy( + c -> c.getCommitMetadata().get().getId().value() + + "_" + + c.getAffectedGlobalId().toString())) + .values() + .stream() + .map(auditLogMapper::toAuditLogDTO) + .sorted(Comparator.comparing(AuditLogDTO::changedAt).reversed()) + .collect(Collectors.toList()); + + boolean hasNext = all.size() > pageable.getPageSize(); + List content = hasNext ? all.subList(0, pageable.getPageSize()) : all; + return new SliceImpl<>(content, pageable, hasNext); + } + + private QueryBuilder buildQueryBuilder(AuditLogFilter filter, Long workspaceId) { + QueryBuilder qb; + if (filter.entityType() != null) { + qb = QueryBuilder.byClass(filter.entityType().getEntityClass()); + } else { + qb = QueryBuilder.anyDomainObject(); + } + + if (workspaceId != null) { + qb = qb.withCommitProperty("workspaceId", workspaceId.toString()); + } + + if (filter.changedBy() != null && !filter.changedBy().isBlank()) { + qb = qb.byAuthor(filter.changedBy()); + } + + if (filter.from() != null) { + qb = qb.from(LocalDateTime.of(filter.from(), LocalTime.MIDNIGHT)); + } + + if (filter.to() != null) { + qb = qb.to(LocalDateTime.of(filter.to(), LocalTime.MAX)); + } + + if (filter.changeType() != null) { + qb = qb.withSnapshotType(toSnapshotType(filter.changeType())); + } + + return qb; + } + + private SnapshotType toSnapshotType(ChangeType changeType) { + return switch (changeType) { + case CREATED -> SnapshotType.INITIAL; + case UPDATED -> SnapshotType.UPDATE; + case DELETED -> SnapshotType.TERMINAL; + }; + } +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/auth/service/impl/AuthServiceImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/auth/service/impl/AuthServiceImpl.java index d154e3de..d7388108 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/auth/service/impl/AuthServiceImpl.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/auth/service/impl/AuthServiceImpl.java @@ -29,16 +29,12 @@ import com.exence.finance.modules.auth.service.TokenValidationService; import com.exence.finance.modules.email.service.EmailLogService; import com.exence.finance.modules.email.service.EmailService; -import com.exence.finance.modules.statistics.dto.Timeframe; -import com.exence.finance.modules.statistics.dto.WidgetType; -import com.exence.finance.modules.statistics.entity.Widget; -import com.exence.finance.modules.statistics.repository.WidgetRepository; +import com.exence.finance.modules.statistics.service.WidgetService; import com.exence.finance.modules.systemsettings.service.SystemSettingsService; import com.exence.finance.modules.workspace.entity.Workspace; import com.exence.finance.modules.workspace.service.WorkspaceMembershipService; import jakarta.servlet.http.HttpServletRequest; import java.time.Instant; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -56,7 +52,7 @@ @Slf4j public class AuthServiceImpl implements AuthService { private final UserRepository userRepository; - private final WidgetRepository widgetRepository; + private final WidgetService widgetService; private final PasswordEncoder passwordEncoder; private final AuthenticationManager authenticationManager; private final UserMapper userMapper; @@ -92,7 +88,7 @@ private AuthenticationResponse internalRegister(RegisterRequest request, Role ro createUserSettings(user); Workspace workspace = workspaceMembershipService.createDefaultWorkspace( user, request.workspaceName(), request.baseCurrency()); - createDefaultDashboardWidget(workspace); + widgetService.createDefaultDashboardWidget(workspace); sendEmailVerification(user); return createAuthenticationResponse(user, workspace.getId()); @@ -228,22 +224,6 @@ private void authenticateUser(LoginRequest request) { } } - private void createDefaultDashboardWidget(Workspace workspace) { - Widget widget = Widget.builder() - .workspace(workspace) - .type(WidgetType.DASHBOARD_BALANCE_TREND) - .title("Balance Trend") - .timeframe(Timeframe.YTD) - .displayOrder(0) - .x(0) - .y(0) - .cols(0) - .rows(0) - .settings(Collections.emptyMap()) - .build(); - widgetRepository.save(widget); - } - private void createUserSettings(User user) { UserSettings settings = UserSettings.builder() .user(user) diff --git a/backend/exence/src/main/java/com/exence/finance/modules/category/entity/Category.java b/backend/exence/src/main/java/com/exence/finance/modules/category/entity/Category.java index 67a600b2..392e187e 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/category/entity/Category.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/category/entity/Category.java @@ -28,6 +28,7 @@ import org.hibernate.annotations.Filter; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; +import org.javers.core.metamodel.annotation.DiffIgnore; @SuperBuilder @Entity @@ -71,6 +72,7 @@ public class Category extends BaseWorkspaceEntity { @Column(name = "note", length = CATEGORY_NOTE_MAX_LENGTH) private String note; + @DiffIgnore @OneToMany(mappedBy = "category", fetch = FetchType.LAZY) private List transactions; } diff --git a/backend/exence/src/main/java/com/exence/finance/modules/category/repository/CategoryRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/category/repository/CategoryRepository.java index cf163a05..92c8cdd6 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/category/repository/CategoryRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/category/repository/CategoryRepository.java @@ -9,12 +9,14 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository +@JaversSpringDataAuditable public interface CategoryRepository extends JpaRepository { @Query("SELECT c FROM Category c WHERE c.id = :id") Optional find(Long id); diff --git a/backend/exence/src/main/java/com/exence/finance/modules/category/service/impl/CategoryServiceImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/category/service/impl/CategoryServiceImpl.java index 40530bb9..7f3e5ff9 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/category/service/impl/CategoryServiceImpl.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/category/service/impl/CategoryServiceImpl.java @@ -15,8 +15,8 @@ import com.exence.finance.modules.category.repository.CategoryRepository; import com.exence.finance.modules.category.service.CategoryService; import com.exence.finance.modules.statistics.event.MaterializedViewRefreshEvent; -import com.exence.finance.modules.workspace.service.WorkspaceMembershipService; import com.exence.finance.modules.transaction.dto.TransactionType; +import com.exence.finance.modules.workspace.service.WorkspaceMembershipService; import java.math.BigDecimal; import java.util.List; import java.util.Map; diff --git a/backend/exence/src/main/java/com/exence/finance/modules/debt/repository/DebtRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/debt/repository/DebtRepository.java index cf30186f..342fe044 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/debt/repository/DebtRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/debt/repository/DebtRepository.java @@ -7,12 +7,14 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository +@JaversSpringDataAuditable public interface DebtRepository extends JpaRepository { @Query("SELECT d FROM Debt d WHERE d.id = :id") diff --git a/backend/exence/src/main/java/com/exence/finance/modules/goal/repository/GoalRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/goal/repository/GoalRepository.java index 708c6e62..bf2a917d 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/goal/repository/GoalRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/goal/repository/GoalRepository.java @@ -6,12 +6,14 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository +@JaversSpringDataAuditable public interface GoalRepository extends JpaRepository { @Query("SELECT g FROM Goal g WHERE g.id = :id") diff --git a/backend/exence/src/main/java/com/exence/finance/modules/investment/entity/Investment.java b/backend/exence/src/main/java/com/exence/finance/modules/investment/entity/Investment.java index dde99d59..2604d067 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/investment/entity/Investment.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/investment/entity/Investment.java @@ -40,10 +40,8 @@ @NoArgsConstructor @AllArgsConstructor @FieldNameConstants -@EqualsAndHashCode( - callSuper = false) -@ToString( - callSuper = true) +@EqualsAndHashCode(callSuper = false) +@ToString(callSuper = true) @Table(name = "investment") @Filter(name = "workspaceFilter", condition = "workspace_id = :workspaceId") public class Investment extends BaseWorkspaceEntity { diff --git a/backend/exence/src/main/java/com/exence/finance/modules/investment/repository/InvestmentRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/investment/repository/InvestmentRepository.java index 30cec59f..7849d031 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/investment/repository/InvestmentRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/investment/repository/InvestmentRepository.java @@ -4,11 +4,13 @@ import java.math.BigDecimal; import java.util.List; import java.util.Optional; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository +@JaversSpringDataAuditable public interface InvestmentRepository extends JpaRepository { @Query("SELECT i FROM Investment i WHERE i.id = :id") diff --git a/backend/exence/src/main/java/com/exence/finance/modules/statistics/service/WidgetService.java b/backend/exence/src/main/java/com/exence/finance/modules/statistics/service/WidgetService.java index 091d7499..839c409c 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/statistics/service/WidgetService.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/statistics/service/WidgetService.java @@ -5,11 +5,14 @@ import com.exence.finance.modules.statistics.dto.WidgetCreateDTO; import com.exence.finance.modules.statistics.dto.response.WidgetDataResponse; import com.exence.finance.modules.statistics.dto.response.WidgetLayoutResponse; +import com.exence.finance.modules.workspace.entity.Workspace; public interface WidgetService { WidgetLayoutResponse getLayout(); + void createDefaultDashboardWidget(Workspace workspace); + WidgetDataResponse getWidgetData(Long widgetId, Timeframe timeframe); WidgetDataResponse getDashboardBalanceTrend(Timeframe timeframe); diff --git a/backend/exence/src/main/java/com/exence/finance/modules/statistics/service/impl/WidgetServiceImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/statistics/service/impl/WidgetServiceImpl.java index 5566899b..3e7ec8f6 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/statistics/service/impl/WidgetServiceImpl.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/statistics/service/impl/WidgetServiceImpl.java @@ -20,9 +20,11 @@ import com.exence.finance.modules.statistics.service.WidgetSettingsValidator; import com.exence.finance.modules.statistics.service.provider.WidgetDataProvider; import com.exence.finance.modules.workspace.context.WorkspaceContextHolder; -import com.exence.finance.modules.workspace.service.WorkspaceService; +import com.exence.finance.modules.workspace.entity.Workspace; +import com.exence.finance.modules.workspace.repository.WorkspaceRepository; import jakarta.annotation.PostConstruct; import java.time.LocalDate; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -42,7 +44,7 @@ public class WidgetServiceImpl implements WidgetService { private final WidgetMapper widgetMapper; private final WidgetRepository widgetRepository; private final StatisticsQueryService statisticsQueryService; - private final WorkspaceService workspaceService; + private final WorkspaceRepository workspaceRepository; private final WidgetSettingsValidator widgetSettingsValidator; private final List providers; @@ -81,7 +83,9 @@ public WidgetLayoutResponse getLayout() { public WidgetLayoutResponse createWidget(WidgetCreateDTO widgetCreateDTO) { widgetSettingsValidator.validate(widgetCreateDTO.settings()); Widget widget = widgetMapper.mapToWidget(widgetCreateDTO); - widget.setWorkspace(workspaceService.getWorkspace(WorkspaceContextHolder.getWorkspaceId())); + widget.setWorkspace(workspaceRepository + .findById(WorkspaceContextHolder.getWorkspaceId()) + .orElseThrow(() -> new ExenceException(ErrorCode.WORKSPACE_NOT_FOUND))); widgetRepository.save(widget); @@ -181,6 +185,24 @@ public WidgetDataResponse getDashboardBalanceTrend(Timeframe timeframe) { return getWidgetData(widget.getId(), timeframe); } + @Override + @WriteTransactional + public void createDefaultDashboardWidget(Workspace workspace) { + Widget widget = Widget.builder() + .workspace(workspace) + .type(WidgetType.DASHBOARD_BALANCE_TREND) + .title("Balance Trend") + .timeframe(Timeframe.YTD) + .displayOrder(0) + .x(0) + .y(0) + .cols(0) + .rows(0) + .settings(Collections.emptyMap()) + .build(); + widgetRepository.save(widget); + } + private Timeframe resolveTimeframe(Timeframe queryParamTimeframe, Widget widget) { if (widget.getType().isYtdOnly()) { return Timeframe.YTD; diff --git a/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/RecurringTransactionRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/RecurringTransactionRepository.java index 9c0eb3be..f5a9be73 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/RecurringTransactionRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/RecurringTransactionRepository.java @@ -5,6 +5,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,6 +14,7 @@ import org.springframework.stereotype.Repository; @Repository +@JaversSpringDataAuditable public interface RecurringTransactionRepository extends JpaRepository { @Query("SELECT rt FROM RecurringTransaction rt WHERE rt.id = :id") diff --git a/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionRepository.java index 8603cd62..0a8f4dbe 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionRepository.java @@ -7,6 +7,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; @@ -14,6 +15,7 @@ import org.springframework.stereotype.Repository; @Repository +@JaversSpringDataAuditable public interface TransactionRepository extends JpaRepository, QuerydslPredicateExecutor { diff --git a/backend/exence/src/main/java/com/exence/finance/modules/workspace/entity/Workspace.java b/backend/exence/src/main/java/com/exence/finance/modules/workspace/entity/Workspace.java index 09c1444f..eda2b647 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/workspace/entity/Workspace.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/workspace/entity/Workspace.java @@ -24,6 +24,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.SuperBuilder; +import org.javers.core.metamodel.annotation.DiffIgnore; @SuperBuilder @Entity @@ -49,10 +50,12 @@ public class Workspace extends BaseAuditableEntity { @Column(name = "name", nullable = false, length = WORKSPACE_NAME_MAX_LENGTH) private String name; + @DiffIgnore @OneToMany(mappedBy = "workspace", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @Builder.Default private List members = new ArrayList<>(); + @DiffIgnore @OneToOne(mappedBy = "workspace", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private WorkspaceSettings settings; } diff --git a/backend/exence/src/main/java/com/exence/finance/modules/workspace/entity/WorkspaceMember.java b/backend/exence/src/main/java/com/exence/finance/modules/workspace/entity/WorkspaceMember.java index f3e16644..6b2afd28 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/workspace/entity/WorkspaceMember.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/workspace/entity/WorkspaceMember.java @@ -25,6 +25,7 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; +import org.javers.core.metamodel.annotation.DiffIgnore; @Entity @Table( @@ -47,10 +48,12 @@ public class WorkspaceMember { @Column(name = "id") private Long id; + @DiffIgnore @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "workspace_id", nullable = false) private Workspace workspace; + @DiffIgnore @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) private User user; @@ -59,6 +62,7 @@ public class WorkspaceMember { @Column(name = "role", nullable = false, length = ROLE_MAX_LENGTH) private WorkspaceRole role; + @DiffIgnore @CreationTimestamp @Column(name = "joined_at", nullable = false, updatable = false) private Instant joinedAt; diff --git a/backend/exence/src/main/java/com/exence/finance/modules/workspace/repository/WorkspaceMemberRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/workspace/repository/WorkspaceMemberRepository.java index 9e6d62a5..4b2d2b00 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/workspace/repository/WorkspaceMemberRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/workspace/repository/WorkspaceMemberRepository.java @@ -3,12 +3,14 @@ import com.exence.finance.modules.workspace.entity.WorkspaceMember; import java.util.List; import java.util.Optional; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository +@JaversSpringDataAuditable public interface WorkspaceMemberRepository extends JpaRepository { Optional findByWorkspaceIdAndUserId(Long workspaceId, Long userId); diff --git a/backend/exence/src/main/java/com/exence/finance/modules/workspace/repository/WorkspaceRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/workspace/repository/WorkspaceRepository.java index 45299216..b83e336b 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/workspace/repository/WorkspaceRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/workspace/repository/WorkspaceRepository.java @@ -2,12 +2,14 @@ import com.exence.finance.modules.workspace.entity.Workspace; import java.util.List; +import org.javers.spring.annotation.JaversSpringDataAuditable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository +@JaversSpringDataAuditable public interface WorkspaceRepository extends JpaRepository { @Query("SELECT w FROM Workspace w JOIN w.members m WHERE m.user.id = :userId") diff --git a/backend/exence/src/main/java/com/exence/finance/modules/workspace/service/impl/WorkspaceServiceImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/workspace/service/impl/WorkspaceServiceImpl.java index fa7b40f9..e24bc421 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/workspace/service/impl/WorkspaceServiceImpl.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/workspace/service/impl/WorkspaceServiceImpl.java @@ -6,6 +6,7 @@ import com.exence.finance.common.exception.ExenceException; import com.exence.finance.modules.auth.entity.User; import com.exence.finance.modules.auth.service.UserService; +import com.exence.finance.modules.statistics.service.WidgetService; import com.exence.finance.modules.workspace.dto.WorkspaceCreateRequest; import com.exence.finance.modules.workspace.dto.WorkspaceGetDTO; import com.exence.finance.modules.workspace.dto.WorkspaceMemberEmailRequest; @@ -31,6 +32,7 @@ public class WorkspaceServiceImpl implements WorkspaceService { private final WorkspaceMembershipService workspaceMembershipService; private final WorkspaceMapper workspaceMapper; private final UserService userService; + private final WidgetService widgetService; @Override @ReadTransactional @@ -46,6 +48,7 @@ public WorkspaceGetDTO createWorkspace(WorkspaceCreateRequest request) { User currentUser = userService.getCurrentUser(); Workspace workspace = workspaceMembershipService.createDefaultWorkspace(currentUser, request.name(), request.baseCurrency()); + widgetService.createDefaultDashboardWidget(workspace); return workspaceMapper.mapToGetDTO(workspace, currentUser.getId()); } diff --git a/backend/exence/src/main/resources/application.yml b/backend/exence/src/main/resources/application.yml index 399a4c87..3658a721 100644 --- a/backend/exence/src/main/resources/application.yml +++ b/backend/exence/src/main/resources/application.yml @@ -66,6 +66,9 @@ jwt: password-reset-token-expiration: 15m cleanup-interval: 3600000 # 1 hour in milliseconds +javers: + algorithm: AS_SET + # logging configuration logging: level: