feat: multi-tenancy

This commit is contained in:
effe 2024-09-14 12:35:29 -04:00
parent d3e0490eee
commit b476cb9846
12 changed files with 93 additions and 63 deletions

View file

@ -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

View file

@ -14,9 +14,9 @@ public class ExpenseFacade {
this.expenseService = expenseService;
}
public void setExpensePaid(Expense expense, TreeGrid<Object> grid) {
public void setExpensePaid(Expense expense, TreeGrid<Object> 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();

View file

@ -18,12 +18,12 @@ public class PersonFacade {
public PersonFacade(ExpenseService expenseService) {
this.expenseService = expenseService;
}
public void setDebtPaid(Person person, TreeGrid<Object> grid) {
public void setDebtPaid(Person person, TreeGrid<Object> grid, Long userId) {
try {
List<Expense> 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<Object> grid) {
public void setCreditPaid(Person person, TreeGrid<Object> grid, Long userId) {
try {
List<Expense> 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);

View file

@ -56,5 +56,5 @@ public interface ExpenseRepository extends JpaRepository<Expense, Long>, JpaSpec
boolean existsByIdAndIsPaidTrue(Long id);
// Find all expenses ordered by date descending
List<Expense> findAllByOrderByDateDesc();
List<Expense> findByUserIdOrderByDateDesc(Long userId);
}

View file

@ -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<Person, Long>, JpaSpecificationExecutor<Person> {
Optional<Person> findByUserId(Long userId);
@Query("SELECT p FROM Person p WHERE p.userId IS NULL")
List<Person> findAllExcludeUser();
Person findByUsername(String username);
@Query("SELECT p FROM Person p WHERE p.username = :username")
Optional<Person> 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<Person> findAllByUserIdExcludingPerson(@Param("userId") Long userId, @Param("username") String username);
List<Person> findByUserId(Long userId);
}

View file

@ -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<User, Long> {
Optional<User> findByUsername(final @Nonnull String username);
}
@Query("SELECT u FROM User u WHERE u.username = :username")
Optional<User> findByUsername(@Param("username") String username);}

View file

@ -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<Expense> findExpensesByUser(final Person person) {
public List<Expense> 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<Expense> findAllOrderByDateDescending() {
return this.expenseRepository.findAllByOrderByDateDesc();
public List<Expense> 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");

View file

@ -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<Person> findAll() {
return this.personRepository.findAll();
public List<Person> findAllByUserId(Long userId) {
return this.personRepository.findByUserId(userId);
}
public Optional<Person> 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<Person> findAllExcludeUsers() {
return this.personRepository.findAllExcludeUser();
public List<Person> 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);
}

View file

@ -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);
}
}

View file

@ -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");

View file

@ -56,6 +56,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
private final BeanValidationBinder<Expense> 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<Person> 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);
}
}
}

View file

@ -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<Person> 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<Person> people = personService.findAllExcludeUsers();
List<Person> 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<Person> 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<Expense> expenses = expenseService.findExpensesByUser(user);
List<Expense> 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);
}
}
}