From b476cb984627e903aab446159ed892da50f9b9be Mon Sep 17 00:00:00 2001 From: effe Date: Sat, 14 Sep 2024 12:35:29 -0400 Subject: [PATCH] feat: multi-tenancy --- .../com/application/munera/data/Person.java | 2 +- .../munera/facades/ExpenseFacade.java | 4 +-- .../munera/facades/PersonFacade.java | 8 +++--- .../repositories/ExpenseRepository.java | 2 +- .../munera/repositories/PersonRepository.java | 20 +++++++++++-- .../munera/repositories/UserRepository.java | 6 ++-- .../munera/services/ExpenseService.java | 26 ++++++++--------- .../munera/services/PersonService.java | 21 ++++++++------ .../munera/services/UserService.java | 7 ++--- .../munera/views/dashboard/DashboardView.java | 10 +++++-- .../munera/views/expenses/ExpensesView.java | 22 +++++++-------- .../munera/views/people/PeopleView.java | 28 ++++++++++++------- 12 files changed, 93 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/application/munera/data/Person.java b/src/main/java/com/application/munera/data/Person.java index 248b18f..0be0117 100644 --- a/src/main/java/com/application/munera/data/Person.java +++ b/src/main/java/com/application/munera/data/Person.java @@ -49,7 +49,7 @@ public class Person { @Column(name = "Username", unique = true) private String username; // This field will link to the User entity - @Column(name = "UserId", unique = true, nullable = false) + @Column(name = "UserId", nullable = false) private Long userId; // Reference to the User entity @Override diff --git a/src/main/java/com/application/munera/facades/ExpenseFacade.java b/src/main/java/com/application/munera/facades/ExpenseFacade.java index 09fa021..65a6c10 100644 --- a/src/main/java/com/application/munera/facades/ExpenseFacade.java +++ b/src/main/java/com/application/munera/facades/ExpenseFacade.java @@ -14,9 +14,9 @@ public class ExpenseFacade { this.expenseService = expenseService; } - public void setExpensePaid(Expense expense, TreeGrid grid) { + public void setExpensePaid(Expense expense, TreeGrid grid, Long userId) { expense.setIsPaid(true); - this.expenseService.update(expense); + this.expenseService.update(expense, userId); Notification.show("Expense " + expense.getName() + " set as paid" ); grid.select(null); grid.getDataProvider().refreshAll(); diff --git a/src/main/java/com/application/munera/facades/PersonFacade.java b/src/main/java/com/application/munera/facades/PersonFacade.java index aabf821..c946d80 100644 --- a/src/main/java/com/application/munera/facades/PersonFacade.java +++ b/src/main/java/com/application/munera/facades/PersonFacade.java @@ -18,12 +18,12 @@ public class PersonFacade { public PersonFacade(ExpenseService expenseService) { this.expenseService = expenseService; } - public void setDebtPaid(Person person, TreeGrid grid) { + public void setDebtPaid(Person person, TreeGrid grid, Long userId) { try { List expenses = expenseService.findExpensesWherePayer(person).stream().toList(); for (Expense expense : expenses) { expense.setIsPaid(true); - expenseService.update(expense); + expenseService.update(expense, userId); } Notification.show("All expenses marked as paid for " + person.getFirstName() + " " + person.getLastName()); grid.select(null); @@ -35,12 +35,12 @@ public class PersonFacade { } } - public void setCreditPaid(Person person, TreeGrid grid) { + public void setCreditPaid(Person person, TreeGrid grid, Long userId) { try { List expenses = expenseService.findExpensesWhereBeneficiary(person).stream().toList(); for (Expense expense : expenses) { expense.setIsPaid(true); - expenseService.update(expense); + expenseService.update(expense, userId); } Notification.show("All expenses marked as paid for " + person.getFirstName() + " " + person.getLastName()); grid.select(null); diff --git a/src/main/java/com/application/munera/repositories/ExpenseRepository.java b/src/main/java/com/application/munera/repositories/ExpenseRepository.java index 508bd45..380cf97 100644 --- a/src/main/java/com/application/munera/repositories/ExpenseRepository.java +++ b/src/main/java/com/application/munera/repositories/ExpenseRepository.java @@ -56,5 +56,5 @@ public interface ExpenseRepository extends JpaRepository, JpaSpec boolean existsByIdAndIsPaidTrue(Long id); // Find all expenses ordered by date descending - List findAllByOrderByDateDesc(); + List findByUserIdOrderByDateDesc(Long userId); } \ No newline at end of file diff --git a/src/main/java/com/application/munera/repositories/PersonRepository.java b/src/main/java/com/application/munera/repositories/PersonRepository.java index 8eb0feb..92edd08 100644 --- a/src/main/java/com/application/munera/repositories/PersonRepository.java +++ b/src/main/java/com/application/munera/repositories/PersonRepository.java @@ -1,16 +1,30 @@ package com.application.munera.repositories; import com.application.munera.data.Person; +import com.application.munera.data.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; public interface PersonRepository extends JpaRepository, JpaSpecificationExecutor { - Optional findByUserId(Long userId); - @Query("SELECT p FROM Person p WHERE p.userId IS NULL") - List findAllExcludeUser(); + Person findByUsername(String username); + + @Query("SELECT p FROM Person p WHERE p.username = :username") + Optional findOptionalByUsername(@Param("username") String username); + + /** + * finds all the people that the logged user has created, minus the person that represents the logged user + * @param userId the logged user id, to get all people connected to id + * @param username the logged username, to filter out + * @return the list people found + */ + @Query("SELECT p FROM Person p WHERE p.userId = :userId AND (p.username IS NULL OR p.username <> :username)") + List findAllByUserIdExcludingPerson(@Param("userId") Long userId, @Param("username") String username); + + List findByUserId(Long userId); } diff --git a/src/main/java/com/application/munera/repositories/UserRepository.java b/src/main/java/com/application/munera/repositories/UserRepository.java index 7a627fe..9c00aa3 100644 --- a/src/main/java/com/application/munera/repositories/UserRepository.java +++ b/src/main/java/com/application/munera/repositories/UserRepository.java @@ -2,6 +2,8 @@ package com.application.munera.repositories; import com.application.munera.data.User; 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; import javax.annotation.Nonnull; @@ -10,5 +12,5 @@ import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { - Optional findByUsername(final @Nonnull String username); -} + @Query("SELECT u FROM User u WHERE u.username = :username") + Optional findByUsername(@Param("username") String username);} diff --git a/src/main/java/com/application/munera/services/ExpenseService.java b/src/main/java/com/application/munera/services/ExpenseService.java index b75aeb3..d031ce3 100644 --- a/src/main/java/com/application/munera/services/ExpenseService.java +++ b/src/main/java/com/application/munera/services/ExpenseService.java @@ -16,7 +16,6 @@ import org.springframework.stereotype.Service; import javax.annotation.Nonnull; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.Year; import java.util.ArrayList; import java.util.Collection; @@ -83,11 +82,11 @@ public class ExpenseService { } /** - * Finds all expenses related to a user, both where the user is a payer and a beneficiary. - * @param person the user of the expenses + * Finds all expenses related to a person, both where the person is a payer and a beneficiary. + * @param person the person of the expenses * @return the list of expenses found */ - public List findExpensesByUser(final Person person) { + public List findExpensesByPerson(final Person person) { // Retrieve expenses where the person is the payer final var payerExpenses = this.findExpensesWherePayer(person); // Retrieve expenses where the person is the beneficiary @@ -118,8 +117,8 @@ public class ExpenseService { * Fetches all expenses ordered by date in descending order. * @return the list of expenses found */ - public List findAllOrderByDateDescending() { - return this.expenseRepository.findAllByOrderByDateDesc(); + public List findAllOrderByDateDescending(Long userId) { + return this.expenseRepository.findByUserIdOrderByDateDesc(userId); } /** @@ -144,7 +143,8 @@ public class ExpenseService { * Updates an existing expense. * @param entity the expense to update */ - public void update(Expense entity) { + public void update(Expense entity, Long userId) { + entity.setUserId(userId); if (Boolean.TRUE.equals(entity.getIsPaid())) entity.setPaymentDate(LocalDate.now()); else entity.setPaymentDate(null); this.setExpenseType(entity); @@ -223,14 +223,12 @@ public class ExpenseService { */ private void setExpenseType(final @Nonnull Expense expense) { // Get the currently logged-in user - UserDetails userDetails = SecurityUtils.getLoggedInUserDetails(); - if (userDetails == null) { - throw new IllegalStateException("No logged-in user found"); - } + final var userDetails = SecurityUtils.getLoggedInUserDetails(); + if (userDetails == null) throw new IllegalStateException("No logged-in user found"); // Fetch the logged-in user - final var loggedInUserId = userRepository.findByUsername(userDetails.getUsername()) - .orElseThrow(() -> new UsernameNotFoundException("User not found")).getId(); - Person loggedInPerson = this.personRepository.findByUserId(loggedInUserId).orElse(null); + final var loggedInUser = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + Person loggedInPerson = this.personRepository.findByUsername(loggedInUser.getUsername()); if (loggedInPerson == null) throw new IllegalStateException("No associated Person entity found for logged-in user"); diff --git a/src/main/java/com/application/munera/services/PersonService.java b/src/main/java/com/application/munera/services/PersonService.java index 8235f36..7a49eef 100644 --- a/src/main/java/com/application/munera/services/PersonService.java +++ b/src/main/java/com/application/munera/services/PersonService.java @@ -2,11 +2,11 @@ package com.application.munera.services; import com.application.munera.data.Expense; import com.application.munera.data.Person; +import com.application.munera.data.User; import com.application.munera.repositories.PersonRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -40,20 +40,22 @@ public class PersonService { * Finds all persons. * @return a collection of all persons */ - public List findAll() { - return this.personRepository.findAll(); + public List findAllByUserId(Long userId) { + return this.personRepository.findByUserId(userId); } - public Optional findByUserId(Long id) { - return this.personRepository.findByUserId(id); + public Person findByUsername(String username) { + return this.personRepository.findByUsername(username); } /** * Finds all people excluding the users'ones. * @return a collection of all persons */ - public List findAllExcludeUsers() { - return this.personRepository.findAllExcludeUser(); + public List findAllExcludeLoggedUser(User user) { + final var userId = user.getId(); + final var username = user.getUsername(); + return this.personRepository.findAllByUserIdExcludingPerson(userId, username); } /** @@ -90,14 +92,15 @@ public class PersonService { */ public Person getLoggedInPerson() { final var user = userService.getLoggedInUser(); - return Objects.requireNonNull(personRepository.findByUserId(user.getId()).orElse(null)); + return Objects.requireNonNull(personRepository.findByUsername(user.getUsername())); } /** * Updates a person in the repository. * @param person the person to update */ - public void update(Person person) { + public void update(Person person, Long userId) { + person.setUserId(userId); this.personRepository.save(person); } diff --git a/src/main/java/com/application/munera/services/UserService.java b/src/main/java/com/application/munera/services/UserService.java index 26819ef..f40d3cf 100644 --- a/src/main/java/com/application/munera/services/UserService.java +++ b/src/main/java/com/application/munera/services/UserService.java @@ -5,7 +5,6 @@ import com.application.munera.data.User; import com.application.munera.repositories.PersonRepository; import com.application.munera.repositories.UserRepository; import jakarta.transaction.Transactional; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import java.util.List; @@ -59,7 +58,7 @@ public class UserService { userRepository.save(userToSave); // Check if the associated person exists for the user - final var existingPersonOptional = personRepository.findByUserId(userToSave.getId()); + final var existingPersonOptional = personRepository.findOptionalByUsername(userToSave.getUsername()); if (existingPersonOptional.isPresent()) { // If person exists, update the person entity @@ -106,7 +105,7 @@ public class UserService { public void delete(final User user) { this.userRepository.delete(user); - final var person = this.personRepository.findByUserId(user.getId()); - person.ifPresent(this.personRepository::delete); + final var person = this.personRepository.findByUsername(user.getUsername()); + this.personRepository.delete(person); } } diff --git a/src/main/java/com/application/munera/views/dashboard/DashboardView.java b/src/main/java/com/application/munera/views/dashboard/DashboardView.java index 5248975..93d06ca 100644 --- a/src/main/java/com/application/munera/views/dashboard/DashboardView.java +++ b/src/main/java/com/application/munera/views/dashboard/DashboardView.java @@ -2,8 +2,10 @@ package com.application.munera.views.dashboard; import com.application.munera.data.Expense; import com.application.munera.data.Person; +import com.application.munera.data.User; import com.application.munera.services.ExpenseService; import com.application.munera.services.PersonService; +import com.application.munera.services.UserService; import com.application.munera.views.MainLayout; import com.nimbusds.jose.shaded.gson.Gson; import com.vaadin.flow.component.html.Div; @@ -28,10 +30,14 @@ public class DashboardView extends Div { private final ExpenseService expenseService; private final PersonService personService; + private final UserService userService; + private final User loggedUser; - public DashboardView(final ExpenseService expenseService, final PersonService personService) { + public DashboardView(final ExpenseService expenseService, final PersonService personService, UserService userService) { this.expenseService = expenseService; this.personService = personService; + this.userService = userService; + loggedUser = userService.getLoggedInUser(); addClassName("highcharts-view"); // Optional CSS class for styling VerticalLayout mainLayout = new VerticalLayout(); @@ -190,7 +196,7 @@ public class DashboardView extends Div { } private String generateNegativeColumnChartScript() { - final var people = personService.findAllExcludeUsers().stream() + final var people = personService.findAllExcludeLoggedUser(loggedUser).stream() .filter(person -> personService.calculateNetBalance(person).compareTo(BigDecimal.ZERO) != 0) .toList(); if (people.isEmpty()) return generatePlaceholderChartScript("bottomLeftChart", "All Payments Settled"); diff --git a/src/main/java/com/application/munera/views/expenses/ExpensesView.java b/src/main/java/com/application/munera/views/expenses/ExpensesView.java index dc5219a..81584c6 100644 --- a/src/main/java/com/application/munera/views/expenses/ExpensesView.java +++ b/src/main/java/com/application/munera/views/expenses/ExpensesView.java @@ -56,6 +56,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver { private final BeanValidationBinder binder; private Expense expense; + private Long userId; private final ExpenseService expenseService; private final CategoryService categoryService; @@ -81,6 +82,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver { this.personService = personService; this.viewsService = viewsService; this.userService = userService; + this.userId = userService.getLoggedInUser().getId(); addClassNames("expenses-view"); // Create UI @@ -101,7 +103,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver { grid.addColumn(new ComponentRenderer<>(this.viewsService::createExpenseBadge)).setHeader("Status").setSortable(true); grid.getColumns().forEach(col -> col.setAutoWidth(true)); - grid.setItems(this.expenseService.findAllOrderByDateDescending()); + grid.setItems(this.expenseService.findAllOrderByDateDescending(userId)); grid.setPaginatorSize(5); grid.setPageSize(22); // setting page size grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); @@ -180,7 +182,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver { try { if (this.expense == null) this.expense = new Expense(); binder.writeBean(this.expense); - expenseService.update(this.expense); + expenseService.update(this.expense, userId); clearForm(); refreshGrid(); Notification.show("Data updated"); @@ -235,13 +237,12 @@ public class ExpensesView extends Div implements BeforeEnterObserver { } private void createEditorLayout(SplitLayout splitLayout) { - final var userId = this.userService.getLoggedInUser().getId(); Div editorLayoutDiv = new Div(); editorLayoutDiv.setClassName("editor-layout"); Div editorDiv = new Div(); editorDiv.setClassName("editor"); editorLayoutDiv.add(editorDiv); - final var people = this.personService.findAll(); + final var people = this.personService.findAllByUserId(userId); FormLayout formLayout = new FormLayout(); name = new TextField("Name"); @@ -292,7 +293,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver { } private void refreshGrid() { - grid.setItems(this.expenseService.findAllOrderByDateDescending()); + grid.setItems(this.expenseService.findAllOrderByDateDescending(userId)); grid.select(null); grid.getDataProvider().refreshAll(); } @@ -309,19 +310,18 @@ public class ExpensesView extends Div implements BeforeEnterObserver { periodInterval.setVisible(isPeriodicChecked); } + //TODO: check and improve this mess pls private void initializeComboBoxes() { // Fetch the logged-in user's Person entity UserDetails userDetails = SecurityUtils.getLoggedInUserDetails(); if (userDetails != null) { String username = userDetails.getUsername(); final var user = this.userService.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found")); - Optional loggedInPerson = personService.findByUserId(user.getId()); - if (loggedInPerson.isPresent()) { - Person person = loggedInPerson.get(); + final var loggedInPerson = personService.findByUsername(user.getUsername()); // Set default values for payer and beneficiary ComboBoxes - payer.setValue(person); - beneficiary.setValue(person); - } + payer.setValue(loggedInPerson); + beneficiary.setValue(loggedInPerson); + } } } \ No newline at end of file diff --git a/src/main/java/com/application/munera/views/people/PeopleView.java b/src/main/java/com/application/munera/views/people/PeopleView.java index c1ba31f..b63bc5f 100644 --- a/src/main/java/com/application/munera/views/people/PeopleView.java +++ b/src/main/java/com/application/munera/views/people/PeopleView.java @@ -2,10 +2,12 @@ package com.application.munera.views.people; import com.application.munera.data.Expense; import com.application.munera.data.Person; +import com.application.munera.data.User; import com.application.munera.facades.ExpenseFacade; import com.application.munera.facades.PersonFacade; import com.application.munera.services.ExpenseService; import com.application.munera.services.PersonService; +import com.application.munera.services.UserService; import com.application.munera.services.ViewsService; import com.application.munera.views.MainLayout; import com.vaadin.flow.component.UI; @@ -56,21 +58,27 @@ public class PeopleView extends Div implements BeforeEnterObserver { private final BeanValidationBinder binder; private Person person; + private User loggedUser; + private Long userId; private final PersonService personService; private final PersonFacade personFacade; private final ExpenseFacade expenseFacade; private final ExpenseService expenseService; private final ViewsService viewsService; + private final UserService userService; private TextField firstName; private TextField lastName; private EmailField email; - public PeopleView(PersonService personService, ExpenseService expenseService, ViewsService viewsService, PersonFacade personFacade, ExpenseFacade expenseFacade) { + public PeopleView(PersonService personService, ExpenseService expenseService, ViewsService viewsService, PersonFacade personFacade, ExpenseFacade expenseFacade, UserService userService) { this.personService = personService; this.expenseService = expenseService; this.viewsService = viewsService; this.personFacade = personFacade; this.expenseFacade = expenseFacade; + this.userService = userService; + loggedUser = userService.getLoggedInUser(); + userId = loggedUser.getId(); addClassNames("expenses-view"); // Create UI @@ -92,12 +100,12 @@ public class PeopleView extends Div implements BeforeEnterObserver { grid.addColumn(new ComponentRenderer<>(persona -> { switch (persona) { case Person person1 -> { - Button setDebtPaidButton = new Button("Set all debt as paid", event -> this.personFacade.setDebtPaid(person1, grid)); + Button setDebtPaidButton = new Button("Set all debt as paid", event -> this.personFacade.setDebtPaid(person1, grid, userId)); setDebtPaidButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY); return setDebtPaidButton; } case Expense expense -> { - Button setExpensePaidButton = new Button("Set as paid", event -> this.expenseFacade.setExpensePaid(expense, grid)); + Button setExpensePaidButton = new Button("Set as paid", event -> this.expenseFacade.setExpensePaid(expense, grid, userId)); setExpensePaidButton.addThemeVariants(ButtonVariant.LUMO_SMALL); if (Boolean.TRUE.equals((expense).getIsPaid())) setExpensePaidButton.setEnabled(false); return setExpensePaidButton; @@ -110,13 +118,13 @@ public class PeopleView extends Div implements BeforeEnterObserver { grid.addColumn(new ComponentRenderer<>(persona -> { if (persona instanceof Person person1) { - Button setCreditPaidButton = new Button("Set all credit as paid", event -> this.personFacade.setCreditPaid(person1, grid)); + Button setCreditPaidButton = new Button("Set all credit as paid", event -> this.personFacade.setCreditPaid(person1, grid, userId)); setCreditPaidButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY); return setCreditPaidButton; } else return new Span(); })); - List people = personService.findAllExcludeUsers(); + List people = personService.findAllExcludeLoggedUser(loggedUser); this.setGridData(people); @@ -148,7 +156,7 @@ public class PeopleView extends Div implements BeforeEnterObserver { try { if (this.person == null) this.person = new Person(); binder.writeBean(this.person); - personService.update(this.person); + personService.update(this.person, userId); clearForm(); refreshGrid(); Notification.show("Data updated"); @@ -263,15 +271,15 @@ public class PeopleView extends Div implements BeforeEnterObserver { } public void setGridData(List people) { - for (Person user : people) { + for (Person person : people) { // Add the person as a root item - grid.getTreeData().addItem(null, user); + grid.getTreeData().addItem(null, person); // Fetch expenses for the current person - List expenses = expenseService.findExpensesByUser(user); + List expenses = expenseService.findExpensesByPerson(person); // Add each expense as a child item under the person - for (Expense expense : expenses) grid.getTreeData().addItem(user, expense); + for (Expense expense : expenses) grid.getTreeData().addItem(person, expense); } } } \ No newline at end of file