Compare commits

...

3 commits

Author SHA1 Message Date
filippo-ferrari
751aa257b1 refactor: packages 2024-07-15 23:18:51 +02:00
filippo-ferrari
73f13934f9 feat: Logout button 2024-07-15 23:12:49 +02:00
filippo-ferrari
03c20db830 feat: LoginView and Spring SecurityConfiguration setup 2024-07-15 23:03:45 +02:00
10 changed files with 203 additions and 7 deletions

View file

@ -62,13 +62,15 @@
<artifactId>line-awesome</artifactId> <artifactId>line-awesome</artifactId>
<version>2.0.0</version> <version>2.0.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId> <artifactId>spring-boot-starter-data-jpa</artifactId>

View file

@ -0,0 +1,72 @@
package com.application.munera;
import com.application.munera.views.login.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);
}
}

View file

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

View file

@ -1,16 +1,24 @@
package com.application.munera.views; package com.application.munera.views;
import com.application.munera.views.categories.CategoriesView;
import com.application.munera.views.dashboard.DashboardView;
import com.application.munera.views.events.EventsView;
import com.application.munera.views.expenses.*; import com.application.munera.views.expenses.*;
import com.application.munera.views.people.PeopleView;
import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle; 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.Footer;
import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Header; import com.vaadin.flow.component.html.Header;
import com.vaadin.flow.component.html.Span; 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.orderedlayout.Scroller;
import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.spring.security.AuthenticationContext;
import com.vaadin.flow.theme.lumo.LumoUtility; import com.vaadin.flow.theme.lumo.LumoUtility;
import org.vaadin.lineawesome.LineAwesomeIcon; import org.vaadin.lineawesome.LineAwesomeIcon;
@ -20,8 +28,10 @@ import org.vaadin.lineawesome.LineAwesomeIcon;
public class MainLayout extends AppLayout { public class MainLayout extends AppLayout {
private H1 viewTitle; private H1 viewTitle;
private final transient AuthenticationContext authContext;
public MainLayout() { public MainLayout(AuthenticationContext authContext) {
this.authContext = authContext;
setPrimarySection(Section.DRAWER); setPrimarySection(Section.DRAWER);
addDrawerContent(); addDrawerContent();
addHeaderContent(); addHeaderContent();
@ -34,7 +44,22 @@ public class MainLayout extends AppLayout {
viewTitle = new H1(); viewTitle = new H1();
viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); 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(true, toggle, viewTitle);
addToNavbar(header);
} }
private void addDrawerContent() { private void addDrawerContent() {

View file

@ -1,4 +1,4 @@
package com.application.munera.views.expenses; package com.application.munera.views.categories;
import com.application.munera.data.Category; import com.application.munera.data.Category;
import com.application.munera.services.CategoryService; import com.application.munera.services.CategoryService;
@ -22,12 +22,14 @@ import com.vaadin.flow.data.binder.BeanValidationBinder;
import com.vaadin.flow.data.binder.ValidationException; import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.*; import com.vaadin.flow.router.*;
import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
import jakarta.annotation.security.PermitAll;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.orm.ObjectOptimisticLockingFailureException;
import java.util.Optional; import java.util.Optional;
@PageTitle("Categories") @PageTitle("Categories")
@PermitAll
@Route(value = "categories/:categoryID?/:action?(edit)", layout = MainLayout.class) @Route(value = "categories/:categoryID?/:action?(edit)", layout = MainLayout.class)
@Uses(Icon.class) @Uses(Icon.class)
public class CategoriesView extends Div implements BeforeEnterObserver { public class CategoriesView extends Div implements BeforeEnterObserver {

View file

@ -1,4 +1,4 @@
package com.application.munera.views.expenses; package com.application.munera.views.dashboard;
import com.application.munera.data.Expense; import com.application.munera.data.Expense;
import com.application.munera.data.Person; import com.application.munera.data.Person;
@ -11,6 +11,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import jakarta.annotation.security.PermitAll;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Year; import java.time.Year;
@ -20,6 +21,7 @@ import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
//@HtmlImport("frontend://styles/shared-styles.html") // If you have custom styles //@HtmlImport("frontend://styles/shared-styles.html") // If you have custom styles
@PermitAll
@PageTitle("Dashboard") @PageTitle("Dashboard")
@Route(value = "dashboard", layout = MainLayout.class) @Route(value = "dashboard", layout = MainLayout.class)
public class DashboardView extends Div { public class DashboardView extends Div {

View file

@ -1,4 +1,4 @@
package com.application.munera.views.expenses; package com.application.munera.views.events;
import com.application.munera.data.Event; import com.application.munera.data.Event;
import com.application.munera.data.Person; import com.application.munera.data.Person;
@ -28,12 +28,14 @@ import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
import jakarta.annotation.security.PermitAll;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.orm.ObjectOptimisticLockingFailureException;
import java.util.Optional; import java.util.Optional;
@PageTitle("Events") @PageTitle("Events")
@PermitAll
@Route(value = "events/:eventID?/:action?(edit)", layout = MainLayout.class) @Route(value = "events/:eventID?/:action?(edit)", layout = MainLayout.class)
@Uses(Icon.class) @Uses(Icon.class)
public class EventsView extends Div implements BeforeEnterObserver { public class EventsView extends Div implements BeforeEnterObserver {

View file

@ -30,6 +30,7 @@ import com.vaadin.flow.data.binder.BeanValidationBinder;
import com.vaadin.flow.data.binder.ValidationException; import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.*; import com.vaadin.flow.router.*;
import jakarta.annotation.security.PermitAll;
import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.vaadin.klaudeta.PaginatedGrid; import org.vaadin.klaudeta.PaginatedGrid;
@ -38,6 +39,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@PermitAll
@PageTitle("Expenses") @PageTitle("Expenses")
@Route(value = "/:expenseID?/:action?(edit)", layout = MainLayout.class) @Route(value = "/:expenseID?/:action?(edit)", layout = MainLayout.class)
@RouteAlias(value = "", layout = MainLayout.class) @RouteAlias(value = "", layout = MainLayout.class)

View file

@ -0,0 +1,54 @@
package com.application.munera.views.login;
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.login.LoginI18n;
import com.vaadin.flow.component.notification.Notification;
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 final LoginForm login = new LoginForm();
public LoginView() {
addClassName("login-view");
setSizeFull();
setJustifyContentMode(JustifyContentMode.CENTER);
setAlignItems(Alignment.CENTER);
login.setAction("login");
// Customize the LoginForm
LoginI18n i18n = LoginI18n.createDefault();
i18n.getForm().setForgotPassword("Forgot password?");
login.setI18n(i18n);
// Add a listener for the Forgot password button
login.addForgotPasswordListener(event -> {
Notification.show("Tough shit, feature aint ready yet!", 3000, Notification.Position.BOTTOM_CENTER);
});
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);
}
}
}

View file

@ -1,4 +1,4 @@
package com.application.munera.views.expenses; package com.application.munera.views.people;
import com.application.munera.data.Person; import com.application.munera.data.Person;
import com.application.munera.services.PersonService; import com.application.munera.services.PersonService;
@ -28,6 +28,7 @@ import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
import jakarta.annotation.security.PermitAll;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.orm.ObjectOptimisticLockingFailureException;
@ -35,6 +36,7 @@ import java.math.BigDecimal;
import java.util.Optional; import java.util.Optional;
@PageTitle("People") @PageTitle("People")
@PermitAll
@Route(value = "people/:personID?/:action?(edit)", layout = MainLayout.class) @Route(value = "people/:personID?/:action?(edit)", layout = MainLayout.class)
@Uses(Icon.class) @Uses(Icon.class)
public class PeopleView extends Div implements BeforeEnterObserver { public class PeopleView extends Div implements BeforeEnterObserver {