From 03c20db8307cecc58fae4904ca7804b60e121b40 Mon Sep 17 00:00:00 2001 From: filippo-ferrari Date: Mon, 15 Jul 2024 23:03:45 +0200 Subject: [PATCH] feat: LoginView and Spring SecurityConfiguration setup --- pom.xml | 6 +- .../munera/SecurityConfiguration.java | 72 +++++++++++++++++++ .../munera/services/SecurityService.java | 33 +++++++++ .../application/munera/views/LoginView.java | 43 +++++++++++ .../application/munera/views/MainLayout.java | 23 +++++- .../munera/views/expenses/CategoriesView.java | 2 + .../munera/views/expenses/DashboardView.java | 2 + .../munera/views/expenses/EventsView.java | 2 + .../munera/views/expenses/ExpensesView.java | 2 + .../munera/views/expenses/PeopleView.java | 2 + 10 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/application/munera/SecurityConfiguration.java create mode 100644 src/main/java/com/application/munera/services/SecurityService.java create mode 100644 src/main/java/com/application/munera/views/LoginView.java diff --git a/pom.xml b/pom.xml index d200d37..e4af5cb 100644 --- a/pom.xml +++ b/pom.xml @@ -62,13 +62,15 @@ line-awesome 2.0.0 - org.postgresql postgresql runtime - + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-data-jpa diff --git a/src/main/java/com/application/munera/SecurityConfiguration.java b/src/main/java/com/application/munera/SecurityConfiguration.java new file mode 100644 index 0000000..a4b406b --- /dev/null +++ b/src/main/java/com/application/munera/SecurityConfiguration.java @@ -0,0 +1,72 @@ +package com.application.munera; + +import com.application.munera.views.LoginView; +import com.vaadin.flow.spring.security.VaadinWebSecurity; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.provisioning.UserDetailsManager; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@EnableWebSecurity +@Configuration +public class SecurityConfiguration + extends VaadinWebSecurity { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // Delegating the responsibility of general configurations + // of http security to the super class. It's configuring + // the followings: Vaadin's CSRF protection by ignoring + // framework's internal requests, default request cache, + // ignoring public views annotated with @AnonymousAllowed, + // restricting access to other views/endpoints, and enabling + // NavigationAccessControl authorization. + // You can add any possible extra configurations of your own + // here (the following is just an example): + + // http.rememberMe().alwaysRemember(false); + + // Configure your static resources with public access before calling + // super.configure(HttpSecurity) as it adds final anyRequest matcher + http.authorizeHttpRequests(auth -> auth.requestMatchers(new AntPathRequestMatcher("/public/**")) + .permitAll()); + + super.configure(http); + + // This is important to register your login view to the + // navigation access control mechanism: + setLoginView(http, LoginView.class); + } + + @Override + public void configure(WebSecurity web) throws Exception { + // Customize your WebSecurity configuration. + super.configure(web); + } + + /** + * Demo UserDetailsManager which only provides two hardcoded + * in memory users and their roles. + * NOTE: This shouldn't be used in real world applications. + */ + @Bean + public UserDetailsManager userDetailsService() { + UserDetails user = + User.withUsername("user") + .password("{noop}user") + .roles("USER") + .build(); + UserDetails admin = + User.withUsername("admin") + .password("{noop}admin") + .roles("ADMIN") + .build(); + return new InMemoryUserDetailsManager(user, admin); + } +} \ No newline at end of file diff --git a/src/main/java/com/application/munera/services/SecurityService.java b/src/main/java/com/application/munera/services/SecurityService.java new file mode 100644 index 0000000..2af72c4 --- /dev/null +++ b/src/main/java/com/application/munera/services/SecurityService.java @@ -0,0 +1,33 @@ +package com.application.munera.services; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinServletRequest; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Component; + +@Component +public class SecurityService { + + private static final String LOGOUT_SUCCESS_URL = "/"; + + public UserDetails getAuthenticatedUser() { + SecurityContext context = SecurityContextHolder.getContext(); + Object principal = context.getAuthentication().getPrincipal(); + if (principal instanceof UserDetails) { + return (UserDetails) context.getAuthentication().getPrincipal(); + } + // Anonymous or no authentication. + return null; + } + + public void logout() { + UI.getCurrent().getPage().setLocation(LOGOUT_SUCCESS_URL); + SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); + logoutHandler.logout( + VaadinServletRequest.getCurrent().getHttpServletRequest(), null, + null); + } +} \ No newline at end of file diff --git a/src/main/java/com/application/munera/views/LoginView.java b/src/main/java/com/application/munera/views/LoginView.java new file mode 100644 index 0000000..eea7a22 --- /dev/null +++ b/src/main/java/com/application/munera/views/LoginView.java @@ -0,0 +1,43 @@ +package com.application.munera.views; + +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.login.LoginForm; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; + + + +@Route("login") +@PageTitle("Login") +@AnonymousAllowed +public class LoginView extends VerticalLayout implements BeforeEnterObserver { + + private LoginForm login = new LoginForm(); + + public LoginView() { + addClassName("login-view"); + setSizeFull(); + + setJustifyContentMode(JustifyContentMode.CENTER); + setAlignItems(Alignment.CENTER); + + login.setAction("login"); + + add(new H1("Munera"), new H2("An expense tracking application"), login); + } + + @Override + public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { + if(beforeEnterEvent.getLocation() + .getQueryParameters() + .getParameters() + .containsKey("error")) { + login.setError(true); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/application/munera/views/MainLayout.java b/src/main/java/com/application/munera/views/MainLayout.java index a078beb..2a71d5f 100644 --- a/src/main/java/com/application/munera/views/MainLayout.java +++ b/src/main/java/com/application/munera/views/MainLayout.java @@ -3,14 +3,18 @@ package com.application.munera.views; import com.application.munera.views.expenses.*; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.html.Footer; import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.Header; import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.Scroller; import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.spring.security.AuthenticationContext; import com.vaadin.flow.theme.lumo.LumoUtility; import org.vaadin.lineawesome.LineAwesomeIcon; @@ -20,8 +24,10 @@ import org.vaadin.lineawesome.LineAwesomeIcon; public class MainLayout extends AppLayout { private H1 viewTitle; + private final transient AuthenticationContext authContext; - public MainLayout() { + public MainLayout(AuthenticationContext authContext) { + this.authContext = authContext; setPrimarySection(Section.DRAWER); addDrawerContent(); addHeaderContent(); @@ -34,7 +40,22 @@ public class MainLayout extends AppLayout { viewTitle = new H1(); viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + // Creating the logout button + Button logout = new Button("Logout", click -> this.authContext.logout()); + + // Adding some padding to the logout button + logout.getStyle().set("padding", "10px"); + + // Creating the header and adding the logout button to the far left + HorizontalLayout header = new HorizontalLayout(logout); + header.setWidthFull(); // Make the header take the full width + header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + header.setJustifyContentMode(FlexComponent.JustifyContentMode.END); // Align items to the start (left) + header.getStyle().set("padding", "0 10px"); // Add padding around the header if needed + + addToNavbar(true, toggle, viewTitle); + addToNavbar(header); } private void addDrawerContent() { diff --git a/src/main/java/com/application/munera/views/expenses/CategoriesView.java b/src/main/java/com/application/munera/views/expenses/CategoriesView.java index 2c4162c..a694221 100644 --- a/src/main/java/com/application/munera/views/expenses/CategoriesView.java +++ b/src/main/java/com/application/munera/views/expenses/CategoriesView.java @@ -22,12 +22,14 @@ import com.vaadin.flow.data.binder.BeanValidationBinder; import com.vaadin.flow.data.binder.ValidationException; import com.vaadin.flow.router.*; import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; +import jakarta.annotation.security.PermitAll; import org.springframework.data.domain.PageRequest; import org.springframework.orm.ObjectOptimisticLockingFailureException; import java.util.Optional; @PageTitle("Categories") +@PermitAll @Route(value = "categories/:categoryID?/:action?(edit)", layout = MainLayout.class) @Uses(Icon.class) public class CategoriesView extends Div implements BeforeEnterObserver { diff --git a/src/main/java/com/application/munera/views/expenses/DashboardView.java b/src/main/java/com/application/munera/views/expenses/DashboardView.java index 8149e16..7608154 100644 --- a/src/main/java/com/application/munera/views/expenses/DashboardView.java +++ b/src/main/java/com/application/munera/views/expenses/DashboardView.java @@ -11,6 +11,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import jakarta.annotation.security.PermitAll; import java.math.BigDecimal; import java.time.Year; @@ -20,6 +21,7 @@ import java.util.*; import java.util.stream.Collectors; //@HtmlImport("frontend://styles/shared-styles.html") // If you have custom styles +@PermitAll @PageTitle("Dashboard") @Route(value = "dashboard", layout = MainLayout.class) public class DashboardView extends Div { diff --git a/src/main/java/com/application/munera/views/expenses/EventsView.java b/src/main/java/com/application/munera/views/expenses/EventsView.java index c38c553..47aabb3 100644 --- a/src/main/java/com/application/munera/views/expenses/EventsView.java +++ b/src/main/java/com/application/munera/views/expenses/EventsView.java @@ -28,12 +28,14 @@ import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; +import jakarta.annotation.security.PermitAll; import org.springframework.data.domain.PageRequest; import org.springframework.orm.ObjectOptimisticLockingFailureException; import java.util.Optional; @PageTitle("Events") +@PermitAll @Route(value = "events/:eventID?/:action?(edit)", layout = MainLayout.class) @Uses(Icon.class) public class EventsView extends Div implements BeforeEnterObserver { 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 4ad3e44..b02b8d1 100644 --- a/src/main/java/com/application/munera/views/expenses/ExpensesView.java +++ b/src/main/java/com/application/munera/views/expenses/ExpensesView.java @@ -30,6 +30,7 @@ import com.vaadin.flow.data.binder.BeanValidationBinder; import com.vaadin.flow.data.binder.ValidationException; import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.router.*; +import jakarta.annotation.security.PermitAll; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.vaadin.klaudeta.PaginatedGrid; @@ -38,6 +39,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +@PermitAll @PageTitle("Expenses") @Route(value = "/:expenseID?/:action?(edit)", layout = MainLayout.class) @RouteAlias(value = "", layout = MainLayout.class) diff --git a/src/main/java/com/application/munera/views/expenses/PeopleView.java b/src/main/java/com/application/munera/views/expenses/PeopleView.java index b7bf2cf..01af164 100644 --- a/src/main/java/com/application/munera/views/expenses/PeopleView.java +++ b/src/main/java/com/application/munera/views/expenses/PeopleView.java @@ -28,6 +28,7 @@ import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; +import jakarta.annotation.security.PermitAll; import org.springframework.data.domain.PageRequest; import org.springframework.orm.ObjectOptimisticLockingFailureException; @@ -35,6 +36,7 @@ import java.math.BigDecimal; import java.util.Optional; @PageTitle("People") +@PermitAll @Route(value = "people/:personID?/:action?(edit)", layout = MainLayout.class) @Uses(Icon.class) public class PeopleView extends Div implements BeforeEnterObserver {