From dbdc6cb5eca1c0d030ac06ac3efb9e7ced4cc2ef Mon Sep 17 00:00:00 2001 From: Janis Blatsios Date: Thu, 27 Jun 2024 11:57:54 +0200 Subject: [PATCH] =?UTF-8?q?podbicie=20springa=20i=20paru=20rzeczy=20w=20po?= =?UTF-8?q?m.xml=20edycja=20debta=20i=20podzia=C5=82=20dodanie=20Partition?= =?UTF-8?q?ed=20cookies=20dla=20CSRFa=20DEPLOY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 8 +- .../frontend/src/app/auth/auth.service.ts | 4 + .../frontend/src/app/auth/jwt.interceptor.ts | 4 +- .../component/common/login/login.component.ts | 2 +- .../add-expense/add-expense.component.ts | 122 ++++++++------ .../multi-user-split.component.ts | 35 +++- .../split-dialog/split-dialog.component.ts | 2 +- .../janis/komornik/config/SecurityConfig.java | 4 +- .../PartitionedCookieTokenRepository.java | 153 ++++++++++++++++++ .../janis/komornik/entities/BaseEntity.java | 3 - .../java/pl/janis/komornik/entities/Debt.java | 4 +- .../komornik/service/ExpenseService.java | 10 +- 12 files changed, 278 insertions(+), 73 deletions(-) create mode 100644 src/main/java/pl/janis/komornik/config/security/PartitionedCookieTokenRepository.java diff --git a/pom.xml b/pom.xml index c4ad20d..f2dd880 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.0 + 3.3.1 pl.janis @@ -16,14 +16,14 @@ 21 1.6.0.Beta2 - 3.3.0 + 3.3.1 3.4.0 - 0.12.5 + 0.12.6 8.4.0 1.18.32 3.0.5 3.14.0 - 6.3.0 + 6.3.1 diff --git a/src/main/frontend/src/app/auth/auth.service.ts b/src/main/frontend/src/app/auth/auth.service.ts index 2ea1ba4..5552dd5 100644 --- a/src/main/frontend/src/app/auth/auth.service.ts +++ b/src/main/frontend/src/app/auth/auth.service.ts @@ -128,4 +128,8 @@ export class AuthService { public isHttpsEnabled() { return this.http.get(`${environment.API_URL}/isHttpsEnabled`); } + + public csrfToken() { + return this.http.get(`${environment.API_URL}/csrf`); + } } diff --git a/src/main/frontend/src/app/auth/jwt.interceptor.ts b/src/main/frontend/src/app/auth/jwt.interceptor.ts index 6a81127..bc48e84 100644 --- a/src/main/frontend/src/app/auth/jwt.interceptor.ts +++ b/src/main/frontend/src/app/auth/jwt.interceptor.ts @@ -33,8 +33,8 @@ export class JwtInterceptor implements HttpInterceptor { if (err instanceof HttpErrorResponse) { if (err.status === 401 || err.status === 403) { console.log('redirect'); - this.authService.logout(); - this.router.navigate(['login']); + // this.authService.logout(); + // this.router.navigate(['login']); } } } diff --git a/src/main/frontend/src/app/component/common/login/login.component.ts b/src/main/frontend/src/app/component/common/login/login.component.ts index e51233b..fa76479 100644 --- a/src/main/frontend/src/app/component/common/login/login.component.ts +++ b/src/main/frontend/src/app/component/common/login/login.component.ts @@ -23,10 +23,10 @@ export class LoginComponent implements OnInit { } ngOnInit(): void { - this.authService.isHttpsEnabled().subscribe(); if (!!this.authService.user.value) { // this.router.navigate(['/group/list']); } + this.authService.csrfToken().subscribe(); this.initForm(); if (!!this.email) { this.loginForm.patchValue({login: this.email}) diff --git a/src/main/frontend/src/app/component/expense/add-expense/add-expense.component.ts b/src/main/frontend/src/app/component/expense/add-expense/add-expense.component.ts index 9bb8f1f..2f20f21 100644 --- a/src/main/frontend/src/app/component/expense/add-expense/add-expense.component.ts +++ b/src/main/frontend/src/app/component/expense/add-expense/add-expense.component.ts @@ -69,7 +69,6 @@ export class AddExpenseComponent implements OnInit, OnDestroy { protected editMode: boolean; private debts: Debt[] = []; private splitDialogRef: MatDialogRef; - private readonly AMOUNT_PATTERN = '^\\d+(?:[.,]\\d{0,2})?$'; constructor(private expenseService: ExpenseService, @@ -102,6 +101,7 @@ export class AddExpenseComponent implements OnInit, OnDestroy { this.patchForm(expense); this.form.patchValue(this.currentExpense); this.loadingService.setLoading(false); + this.dataSharingService.amount.set(expense.amount); }); } @@ -183,17 +183,37 @@ export class AddExpenseComponent implements OnInit, OnDestroy { this.onCancel(); } - private initForm() { - this.form = new FormGroup({ - amount: new FormControl(this.calculateAmount(this.currentExpense), [Validators.required, Validators.pattern(this.AMOUNT_PATTERN)]), - description: new FormControl(this.currentExpense?.description ?? null, Validators.required), - currency: new FormControl(this.defaultCurrency, Validators.required), - name: this.userName, - category: new FormControl(this.currentExpense?.categoryId ?? null), - group: new FormControl(this.data.groupId, Validators.required), - date: new FormControl(this.currentExpense?.date ?? new Date(), Validators.required) + openSplitDialog(usersOriginalList: User[]) { + if (this.splitDialogRef && + (this.splitDialogRef as MatDialogRef)?.getState() === 0 || this.dialog.openDialogs.length > 1) { + return; + } + const config = { + data: { + users: usersOriginalList, + currentUser: this.payer, + currency: this.form.value.currency, + existingDebts: this.debts + }, + hasBackdrop: false, + width: '400px', + position: {left: '68%'}, + panelClass: 'slide-in-from-right' + }; + if (this.users.length > 2) { + this.splitDialogRef = this.dialog.open(MultiUserSplitComponent, config); + } else { + this.splitDialogRef = this.dialog.open(SplitDialogComponent, config); + } + this.splitDialogRef.afterClosed().subscribe(split => { + console.log(split); + if (split === undefined) { + return; + } + this.splitHow = split.text; + this.betweenWho = ""; + this.debts = split.debts; }); - this.listenForAmountChange(); } openPayerDialog(payer: User, usersOriginalList: User[]) { @@ -260,32 +280,16 @@ export class AddExpenseComponent implements OnInit, OnDestroy { }); } - openSplitDialog(usersOriginalList: User[]) { - if (this.splitDialogRef && - (this.splitDialogRef as MatDialogRef)?.getState() === 0 || this.dialog.openDialogs.length > 1) { - return; + updateValue(value: string) { + const amount = this.sanitizeAmount(value); + let parseInt = +amount; + if (isNaN(parseInt)) { + parseInt = 0; } - const config = { - data: {users: usersOriginalList, currentUser: this.payer, currency: this.form.value.currency}, - hasBackdrop: false, - width: '400px', - position: {left: '68%'}, - panelClass: 'slide-in-from-right' - }; - if (this.users.length > 2) { - this.splitDialogRef = this.dialog.open(MultiUserSplitComponent, config); - } else { - this.splitDialogRef = this.dialog.open(SplitDialogComponent, config); + if (this.editMode) { + this.updateDebts(parseInt); } - this.splitDialogRef.afterClosed().subscribe(split => { - console.log(split); - if (split === undefined) { - return; - } - this.splitHow = split.text; - this.betweenWho = ""; - this.debts = split.debts; - }); + this.dataSharingService.amount.set(parseInt); } sanitizeInput(amount: string) { @@ -316,10 +320,41 @@ export class AddExpenseComponent implements OnInit, OnDestroy { }); } + updateDebts(currentExpenseAmount: number) { + if (this.debts.length > 0) { + const myDue = -((currentExpenseAmount / (this.users.length)) * (this.users.length - 1)).toFixed(2); + const other = +(currentExpenseAmount / (this.users.length)).toFixed(2) + this.debts.map((debt) => { + if (debt.to.id === this.payer.id) { + debt.amount = myDue; + } else { + debt.amount = other; + } + }); + } + } + + private sanitizeAmount(amount: string) { + return amount.toString().replace(/,/g, '.'); + } + + private initForm() { + this.form = new FormGroup({ + amount: new FormControl(this.calculateTotalAmount(this.currentExpense), [Validators.required, Validators.pattern(this.AMOUNT_PATTERN)]), + description: new FormControl(this.currentExpense?.description ?? null, Validators.required), + currency: new FormControl(this.defaultCurrency, Validators.required), + name: this.userName, + category: new FormControl(this.currentExpense?.categoryId ?? null), + group: new FormControl(this.data.groupId, Validators.required), + date: new FormControl(this.currentExpense?.date ?? new Date(), Validators.required) + }); + this.listenForAmountChange(); + } + private patchForm(expense: Expense) { this.editMode = true; this.form.patchValue({ - amount: this.calculateAmount(expense), + amount: this.calculateTotalAmount(expense), description: expense.description, currency: expense.currency, category: expense.categoryId, @@ -329,19 +364,6 @@ export class AddExpenseComponent implements OnInit, OnDestroy { this.form.addControl("id", new FormControl(expense.id)); } - private sanitizeAmount(amount: string) { - return amount.toString().replace(/,/g, '.'); - } - - updateValue(value: string) { - const amount = this.sanitizeAmount(value); - let parseInt = +amount; - if (isNaN(parseInt)) { - parseInt = 0; - } - this.dataSharingService.amount.set(parseInt); - } - private listenForAmountChange() { this.form.controls['amount'].valueChanges.subscribe((val: string) => { if (!val.toString().match(this.AMOUNT_PATTERN)) { @@ -352,7 +374,7 @@ export class AddExpenseComponent implements OnInit, OnDestroy { }); } - private calculateAmount(expense: Expense): number | string { + private calculateTotalAmount(expense: Expense): number | string { if (!expense?.debt) { return ""; } diff --git a/src/main/frontend/src/app/component/expense/dialogs/multi-user-split/multi-user-split.component.ts b/src/main/frontend/src/app/component/expense/dialogs/multi-user-split/multi-user-split.component.ts index 45db0f1..39ebe7e 100644 --- a/src/main/frontend/src/app/component/expense/dialogs/multi-user-split/multi-user-split.component.ts +++ b/src/main/frontend/src/app/component/expense/dialogs/multi-user-split/multi-user-split.component.ts @@ -30,9 +30,10 @@ export class MultiUserSplitComponent implements OnInit, AfterViewInit { private participants = ''; amountValid = false; private wasChanged = false; + private editMode: boolean; constructor( - @Inject(MAT_DIALOG_DATA) public data: { users: User[], currentUser: User, currency: string }, + @Inject(MAT_DIALOG_DATA) public data: { users: User[], currentUser: User, currency: string, existingDebts: Debt[] }, public dialogRef: MatDialogRef, private fb: FormBuilder, private snackbarService: SnackbarService, @@ -49,6 +50,9 @@ export class MultiUserSplitComponent implements OnInit, AfterViewInit { } ngOnInit() { + if (this.data.existingDebts.length > 0) { + this.editMode = true; + } this.numberForm = this.fb.group({}); const divideMap = this.divideCurrencyEvenly(this.amount, this.data.users.length); this.data.users.forEach((user, index) => { @@ -79,8 +83,14 @@ export class MultiUserSplitComponent implements OnInit, AfterViewInit { this.snackbarService.displayError("Suma musi być równa: " + this.amount + " aktualnie: " + this.getSum()); return; } + let calculatedDebts = this.recalculate(); + console.log(this.data.existingDebts); + if (this.editMode) { + this.assignIdsToDebts(calculatedDebts, this.data.existingDebts); + console.log(calculatedDebts); + } this.dialogRef.close({ - debts: this.recalculate(), + debts: calculatedDebts, text: this.participants }); } @@ -154,11 +164,28 @@ export class MultiUserSplitComponent implements OnInit, AfterViewInit { let totalValue: number = 0; debtMap.forEach((value, user) => { if (user.id !== currentUser.id) { - debts.push({from: user, to: currentUser, amount: value, currency: this.data.currency}); + let debtForOtherUser: Debt = {from: user, to: currentUser, amount: value, currency: this.data.currency}; + debts.push(debtForOtherUser); totalValue += value; } }); - debts.push({from: currentUser, to: currentUser, amount: -totalValue, currency: this.data.currency}); + let debtForCurrenUser: Debt = { + from: currentUser, + to: currentUser, + amount: -totalValue, + currency: this.data.currency + }; + debts.push(debtForCurrenUser); return debts; } + + private assignIdsToDebts(calculatedDebts: Debt[], existingDebts: Debt[]) { + existingDebts.forEach(debt => { + calculatedDebts.map(calculatedDebt => { + if (debt.from.id === calculatedDebt.from.id && debt.to.id === calculatedDebt.to.id) { + calculatedDebt.id = debt.id; + } + }) + }) + } } diff --git a/src/main/frontend/src/app/component/expense/dialogs/split-dialog/split-dialog.component.ts b/src/main/frontend/src/app/component/expense/dialogs/split-dialog/split-dialog.component.ts index 6baafdd..103b4be 100644 --- a/src/main/frontend/src/app/component/expense/dialogs/split-dialog/split-dialog.component.ts +++ b/src/main/frontend/src/app/component/expense/dialogs/split-dialog/split-dialog.component.ts @@ -27,7 +27,7 @@ export class SplitDialogComponent implements OnInit, AfterViewInit { amount = this.dataSharingService.amount; constructor( - @Inject(MAT_DIALOG_DATA) public data: { users: User[], currentUser: User, currency: string }, + @Inject(MAT_DIALOG_DATA) public data: { users: User[], currentUser: User, currency: string, existingDebts: Debt[] }, public dialogRef: MatDialogRef, private fb: FormBuilder, private dataSharingService: DataSharingService, diff --git a/src/main/java/pl/janis/komornik/config/SecurityConfig.java b/src/main/java/pl/janis/komornik/config/SecurityConfig.java index 4035ef2..bf01180 100644 --- a/src/main/java/pl/janis/komornik/config/SecurityConfig.java +++ b/src/main/java/pl/janis/komornik/config/SecurityConfig.java @@ -22,11 +22,11 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; import org.springframework.web.cors.CorsConfiguration; import pl.janis.komornik.config.security.PartitionedCookieLogoutHandler; +import pl.janis.komornik.config.security.PartitionedCookieTokenRepository; import pl.janis.komornik.filter.CsrfCookieFilter; import pl.janis.komornik.filter.SpaWebFilter; import pl.janis.komornik.service.UserService; @@ -48,7 +48,7 @@ public SecurityConfig(@Lazy Filter jwtAuthFilter, @Lazy UserService userService) @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository(); + PartitionedCookieTokenRepository cookieCsrfTokenRepository = new PartitionedCookieTokenRepository(); cookieCsrfTokenRepository.setCookieCustomizer(c -> c.secure(true).httpOnly(true).sameSite("none")); http.securityContext(context -> context.requireExplicitSave(false)) // .requiresChannel(channel -> channel.anyRequest().requiresSecure()) diff --git a/src/main/java/pl/janis/komornik/config/security/PartitionedCookieTokenRepository.java b/src/main/java/pl/janis/komornik/config/security/PartitionedCookieTokenRepository.java new file mode 100644 index 0000000..e933b0f --- /dev/null +++ b/src/main/java/pl/janis/komornik/config/security/PartitionedCookieTokenRepository.java @@ -0,0 +1,153 @@ +package pl.janis.komornik.config.security; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseCookie; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.util.WebUtils; + +import java.util.UUID; +import java.util.function.Consumer; + +import static org.apache.tomcat.util.descriptor.web.Constants.COOKIE_PARTITIONED_ATTR; +import static org.apache.tomcat.util.descriptor.web.Constants.COOKIE_SAME_SITE_ATTR; + +public class PartitionedCookieTokenRepository implements CsrfTokenRepository { + static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN"; + + static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; + + static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN"; + + private static final String CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME = CookieCsrfTokenRepository.class.getName() + .concat(".REMOVED"); + + private final String parameterName = DEFAULT_CSRF_PARAMETER_NAME; + + private final String headerName = DEFAULT_CSRF_HEADER_NAME; + + private String cookieName = DEFAULT_CSRF_COOKIE_NAME; + + private String cookiePath; + + private String cookieDomain; + + private Boolean secure; + + private Consumer cookieCustomizer = (builder) -> { + }; + + /** + * Add a {@link Consumer} for a {@code ResponseCookieBuilder} that will be invoked for + * each cookie being built, just before the call to {@code build()}. + * + * @param cookieCustomizer consumer for a cookie builder + * @since 6.1 + */ + public void setCookieCustomizer(Consumer cookieCustomizer) { + Assert.notNull(cookieCustomizer, "cookieCustomizer must not be null"); + this.cookieCustomizer = cookieCustomizer; + } + + @Override + public CsrfToken generateToken(HttpServletRequest request) { + return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); + } + + @Override + public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { + String tokenValue = (token != null) ? token.getToken() : ""; + + int cookieMaxAge = -1; + boolean cookieHttpOnly = true; + ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie.from(this.cookieName, tokenValue) + .secure((this.secure != null) ? this.secure : request.isSecure()) + .path(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request)) + .maxAge((token != null) ? cookieMaxAge : 0) + .httpOnly(cookieHttpOnly) + .domain(this.cookieDomain); + + this.cookieCustomizer.accept(cookieBuilder); + + Cookie cookie = mapToCookie(cookieBuilder.build()); + cookie.setAttribute(COOKIE_PARTITIONED_ATTR, "true"); + cookie.setAttribute(COOKIE_SAME_SITE_ATTR, "None"); + response.addCookie(cookie); + + // Set request attribute to signal that response has blank cookie value, + // which allows loadToken to return null when token has been removed + if (!StringUtils.hasLength(tokenValue)) { + request.setAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME, Boolean.TRUE); + } else { + request.removeAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME); + } + } + + @Override + public CsrfToken loadToken(HttpServletRequest request) { + // Return null when token has been removed during the current request + // which allows loadDeferredToken to re-generate the token + if (Boolean.TRUE.equals(request.getAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME))) { + return null; + } + Cookie cookie = WebUtils.getCookie(request, this.cookieName); + if (cookie == null) { + return null; + } + String token = cookie.getValue(); + if (!StringUtils.hasLength(token)) { + return null; + } + return new DefaultCsrfToken(this.headerName, this.parameterName, token); + } + + /** + * Sets the name of the cookie that the expected CSRF token is saved to and read from. + * + * @param cookieName the name of the cookie that the expected CSRF token is saved to + * and read from + */ + public void setCookieName(String cookieName) { + Assert.notNull(cookieName, "cookieName cannot be null"); + this.cookieName = cookieName; + } + + + private String getRequestContext(HttpServletRequest request) { + String contextPath = request.getContextPath(); + return (!contextPath.isEmpty()) ? contextPath : "/"; + } + + /** + * Factory method to conveniently create an instance that creates cookies where + * {@link Cookie#isHttpOnly()} is set to false. + * + * @return an instance of CookieCsrfTokenRepository that creates cookies where + * {@link Cookie#isHttpOnly()} is set to false. + */ + + private String createNewToken() { + return UUID.randomUUID().toString(); + } + + private Cookie mapToCookie(ResponseCookie responseCookie) { + Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue()); + cookie.setSecure(responseCookie.isSecure()); + cookie.setPath(responseCookie.getPath()); + cookie.setMaxAge((int) responseCookie.getMaxAge().getSeconds()); + cookie.setHttpOnly(responseCookie.isHttpOnly()); + if (StringUtils.hasLength(responseCookie.getDomain())) { + cookie.setDomain(responseCookie.getDomain()); + } + if (StringUtils.hasText(responseCookie.getSameSite())) { + cookie.setAttribute("SameSite", responseCookie.getSameSite()); + } + return cookie; + } +} diff --git a/src/main/java/pl/janis/komornik/entities/BaseEntity.java b/src/main/java/pl/janis/komornik/entities/BaseEntity.java index b5b89bc..a04a0ae 100644 --- a/src/main/java/pl/janis/komornik/entities/BaseEntity.java +++ b/src/main/java/pl/janis/komornik/entities/BaseEntity.java @@ -21,9 +21,6 @@ public class BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; - @Version - private Integer version; - @CreatedDate @Column(updatable = false, nullable = false) private LocalDateTime createdDate; diff --git a/src/main/java/pl/janis/komornik/entities/Debt.java b/src/main/java/pl/janis/komornik/entities/Debt.java index 833426a..31a384d 100644 --- a/src/main/java/pl/janis/komornik/entities/Debt.java +++ b/src/main/java/pl/janis/komornik/entities/Debt.java @@ -29,11 +29,11 @@ public class Debt extends BaseEntity { @Column(name = "currency", nullable = false) private String currency; - @ManyToOne(cascade = CascadeType.MERGE) + @ManyToOne(cascade = CascadeType.DETACH) @JoinColumn(name = "from_user_id") private User userFrom; - @ManyToOne(cascade = CascadeType.MERGE) + @ManyToOne(cascade = CascadeType.DETACH) @JoinColumn(name = "to_user_id") private User userTo; diff --git a/src/main/java/pl/janis/komornik/service/ExpenseService.java b/src/main/java/pl/janis/komornik/service/ExpenseService.java index 02c2909..1ed9d34 100644 --- a/src/main/java/pl/janis/komornik/service/ExpenseService.java +++ b/src/main/java/pl/janis/komornik/service/ExpenseService.java @@ -30,6 +30,7 @@ public class ExpenseService { private final UserService userService; private final GroupService groupService; private final NBPExchangeService nbpExchangeService; + private final DebtService debtService; public ExpenseDto findById(int id) { return expenseMapper.toDto(expenseRepository.findById(id).orElseThrow(() -> new ElementDoesNotExistException("No results"))); @@ -120,15 +121,16 @@ public void deleteExpense(int id) { @Transactional public ExpenseDto saveExpense(ExpenseDto expenseDto, User currentUser) { groupService.checkIfUserBelongsToGroup(currentUser.getId(), expenseDto.groupId()); - final Expense expense = expenseMapper.toEntity(expenseDto); ExpenseDto dto; - if (expense.getId() != null) { - dto = expenseRepository.findById(expense.getId()).map(storedExpense -> { - storedExpense = expenseMapper.toEntity(expense); + if (expenseDto.id() != null) { + dto = expenseRepository.findById(expenseDto.id()).map(storedExpense -> { + storedExpense = expenseMapper.toEntity(expenseDto); + storedExpense.getDebt().forEach(debtService::save); return expenseMapper.toDto(expenseRepository.save(storedExpense)); }).get(); } else { + final Expense expense = expenseMapper.toEntity(expenseDto); dto = expenseMapper.toDto(expenseRepository.save(expense)); } return dto;