Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fcfd67e
Add application security model API
peholmst May 27, 2025
b773a15
Reformat with spotless
peholmst May 27, 2025
abd99ca
Add dev profile activation
peholmst May 27, 2025
eb02b76
Add dev users
peholmst May 28, 2025
4964780
Flow login and dev security config
peholmst May 28, 2025
57696f6
Add security features to MainLayout
peholmst May 28, 2025
5e5cf84
Use Flow dev login view also in Hilla and Hybrid apps
peholmst May 28, 2025
8a36eac
Add authorities to AppUserPrincipal
peholmst May 28, 2025
2778d23
Add Hilla AuthProvider
peholmst May 28, 2025
418d9e1
Reformat
peholmst May 28, 2025
9d64a14
Protect Task Management by default
peholmst May 28, 2025
f6b8873
Add documentation to CurrentUserService
peholmst May 28, 2025
48d36e4
Implement user menu in Hilla
peholmst May 28, 2025
40f382c
Merge branch 'v24.8' into default-security-config
peholmst May 30, 2025
8be60bd
Merge branch 'v24.8' into default-security-config
peholmst Jun 2, 2025
c03c80e
Merge branch 'v24.8' into default-security-config
peholmst Jun 4, 2025
329e5c8
Add Control Center security configuration
peholmst Jun 5, 2025
bfb650b
Remove AppUserInfoLookup to keep things simple (for now)
peholmst Jun 5, 2025
b9e7e7e
Add Javadocs to security package
peholmst Jun 5, 2025
e8c956e
Improve tests
peholmst Jun 5, 2025
cd9b87c
Exclude frontend security
peholmst Jun 5, 2025
8595b87
Add CC dependency
peholmst Jun 5, 2025
83aa2b7
Add comment about Flyway
peholmst Jun 5, 2025
c018ecc
Programmatically navigate to user profile URL
peholmst Jun 5, 2025
10f7aa3
Use simple form login instead of Flow view
peholmst Jun 5, 2025
f547571
Add missing VaadinAwareSecurityContextHolderStrategyConfiguration
peholmst Jun 6, 2025
06c1ef1
Reformat
peholmst Jun 6, 2025
97784b1
Get the SecurityContextHolderStrategy as a bean rather than using Sec…
peholmst Jun 6, 2025
b6075cf
Move CurrentUserService to its own package
peholmst Jun 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assembly/src/assembly/empty-skeleton.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
<exclude>src/main/bundles/**</exclude>
<exclude>src/main/frontend/generated/**</exclude>
<exclude>src/main/frontend/components/**</exclude>
<exclude>src/main/frontend/security/**</exclude>
<exclude>src/main/frontend/views/**</exclude>
<exclude>src/main/frontend/index.tsx</exclude>
<exclude>src/main/java/**</exclude>
<exclude>src/test/java/**</exclude>
</excludes>
Expand Down
5 changes: 5 additions & 0 deletions assembly/src/assembly/flow-skeleton.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<fileSet>
<outputDirectory>src/main/java</outputDirectory>
<directory>${basedir}/target/generated-sources/flow</directory>
<excludes>
<exclude>com/example/application/security/hilla/**</exclude>
</excludes>
</fileSet>
<fileSet>
<outputDirectory/>
Expand All @@ -30,7 +33,9 @@
<exclude>src/main/bundles/**</exclude>
<exclude>src/main/frontend/generated/**</exclude>
<exclude>src/main/frontend/components/**</exclude>
<exclude>src/main/frontend/security/**</exclude>
<exclude>src/main/frontend/views/**</exclude>
<exclude>src/main/frontend/index.tsx</exclude>
<exclude>src/main/java/**</exclude> <!-- Included from generated-sources/flow instead -->
</excludes>
</fileSet>
Expand Down
22 changes: 22 additions & 0 deletions walking-skeleton/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>
<dependency>
<!-- Remove if you don't intend to use Vaadin Control Center.
If so, also remove the `security.cc` package. -->
<groupId>com.vaadin</groupId>
<artifactId>control-center-starter</artifactId>
<version>1.3.0.beta2</version> <!-- TODO Remove version and exclusions! -->
<exclusions>
<exclusion>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- Persistence -->
<dependency>
Expand All @@ -86,6 +99,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
Expand All @@ -98,6 +115,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down
18 changes: 18 additions & 0 deletions walking-skeleton/src/main/frontend/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router';
import { router } from 'Frontend/generated/routes.js';
import { AuthProvider } from 'Frontend/security/auth';

function App() {
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
);
}

const outlet = document.getElementById('outlet')!;
let root = (outlet as any)._root ?? createRoot(outlet);
(outlet as any)._root = root;
root.render(createElement(App));
6 changes: 6 additions & 0 deletions walking-skeleton/src/main/frontend/security/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { configureAuth } from '@vaadin/hilla-react-auth';
import { CurrentUserService } from 'Frontend/generated/endpoints';

const auth = configureAuth(CurrentUserService.getUserInfo);
export const useAuth = auth.useAuth;
export const AuthProvider = auth.AuthProvider;
35 changes: 26 additions & 9 deletions walking-skeleton/src/main/frontend/views/@layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {
SideNav,
SideNavItem,
} from '@vaadin/react-components';
import { Suspense } from 'react';
import { Suspense, useMemo } from 'react';
import { createMenuItems } from '@vaadin/hilla-file-router/runtime.js';
import { useAuth } from 'Frontend/security/auth';
import { ViewConfig } from '@vaadin/hilla-file-router/types.js';

function Header() {
// TODO Replace with real application logo and name
Expand Down Expand Up @@ -41,22 +43,33 @@ function MainMenu() {
);
}

type UserMenuItem = MenuBarItem<{ action?: () => void }>;
type UserMenuItem = MenuBarItem<{ action?: () => void | Promise<void> }>;

function UserMenu() {
// TODO Replace with real user information and actions
const { logout, state } = useAuth();

const fullName = state.user?.fullName;
const pictureUrl = state.user?.pictureUrl;
const profileUrl = state.user?.profileUrl;

const children: Array<UserMenuItem> = useMemo(() => {
const items: Array<UserMenuItem> = [];
if (profileUrl) {
items.push({ text: 'View Profile', action: () => window.open(profileUrl, 'blank')?.focus() });
}
// TODO Add additional items to the user menu if needed
items.push({ text: 'Logout', action: logout });
return items;
}, [profileUrl, logout]);

const items: Array<UserMenuItem> = [
{
component: (
<>
<Avatar theme="xsmall" name="John Smith" colorIndex={5} className="mr-s" /> John Smith
<Avatar theme="xsmall" img={pictureUrl} name={fullName} colorIndex={5} className="mr-s" /> {fullName}
</>
),
children: [
{ text: 'View Profile', disabled: true, action: () => console.log('View Profile') },
{ text: 'Manage Settings', disabled: true, action: () => console.log('Manage Settings') },
{ text: 'Logout', disabled: true, action: () => console.log('Logout') },
],
children: children,
},
];
const onItemSelected = (event: MenuBarItemSelectedEvent<UserMenuItem>) => {
Expand All @@ -67,6 +80,10 @@ function UserMenu() {
);
}

export const config: ViewConfig = {
loginRequired: true,
};

export default function MainLayout() {
return (
<AppLayout primarySection="drawer">
Expand Down
1 change: 1 addition & 0 deletions walking-skeleton/src/main/frontend/views/task-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const config: ViewConfig = {
order: 1,
title: 'Task List',
},
loginRequired: true,
};

const dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,47 @@

import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.theme.Theme;
import org.springframework.boot.SpringApplication;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;

import java.time.Clock;
import java.util.Arrays;

/**
* Main Spring Boot application class for the Vaadin application.
*
* <h2>Automatic Development Profile Activation</h2> This application automatically activates the "dev" Spring profile
* when:
* <ul>
* <li>Vaadin is running in development mode (vaadin-dev-server dependency is present)</li>
* <li>No Spring profiles have been explicitly configured</li>
* </ul>
*
* <p>
* If you need to use explicit profile management, remove the custom initializer from the {@link #main(String[])} method
* and use standard Spring profile activation:
* </p>
*
* <!-- spotless:off-->
* <pre>
* {@code
* // Replace the current main method with:
* public static void main(String[] args) {
* SpringApplication.run(Application.class, args);
* }
*
* // Then use explicit profile activation:
* java -Dspring.profiles.active=prod -jar your-app.jar
* // or set SPRING_PROFILES_ACTIVE environment variable
* }
* </pre>
* <!-- spotless:on -->
*
* @see #enableDevelopmentModeIfNeeded(ConfigurableApplicationContext)
*/
@SpringBootApplication
@Theme("default")
public class Application implements AppShellConfigurator {
Expand All @@ -18,7 +53,65 @@ public Clock clock() {
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
new SpringApplicationBuilder(Application.class).initializers(Application::enableDevelopmentModeIfNeeded)
.run(args);
}

}
/**
* Conditionally activates the "dev" Spring profile based on Vaadin's runtime mode.
*
* <p>
* This method is called during Spring context initialization and will activate the "dev" profile if and only if:
* </p>
* <ul>
* <li>No Spring profiles are already active (excluding the default "default" profile)</li>
* <li>Vaadin is detected to be running in development mode</li>
* </ul>
*
* <p>
* <strong>Note:</strong> If you prefer explicit profile management, remove this initializer from the main method
* and use standard Spring profile activation methods.
* </p>
*
* @param context
* the Spring application context being initialized
* @see #isVaadinInDevelopmentMode()
*/
private static void enableDevelopmentModeIfNeeded(ConfigurableApplicationContext context) {
var environment = context.getEnvironment();

// Check if any profiles are already explicitly set
var activeProfiles = environment.getActiveProfiles();
var defaultProfiles = environment.getDefaultProfiles();

var hasExplicitProfiles = activeProfiles.length > 0
|| (defaultProfiles.length > 0 && !Arrays.equals(defaultProfiles, new String[] { "default" }));

if (!hasExplicitProfiles && isVaadinInDevelopmentMode()) {
LoggerFactory.getLogger(Application.class).warn("Automatically enabling the DEVELOPMENT profile");
environment.setActiveProfiles("dev");
}
}

/**
* Determines if Vaadin is running in development mode by checking for the presence of development-specific classes
* in the classpath.
*
* <p>
* This method uses classpath inspection rather than system properties because it's more reliable - the
* vaadin-dev-server dependency should only be present during development builds, not in production deployments.
* </p>
*
* @return {@code true} if Vaadin development mode is detected, {@code false} otherwise
*/
private static boolean isVaadinInDevelopmentMode() {
try {
// This class is in the 'vaadin-dev-server' dependency, which should be in the classpath when running
// in development mode, but not when running in production mode.
Class.forName("com.vaadin.base.devserver.ServerInfo");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.example.application.base.ui.view;

import com.example.application.security.CurrentUser;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
Expand All @@ -16,6 +18,7 @@
import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.menu.MenuConfiguration;
import com.vaadin.flow.server.menu.MenuEntry;
import com.vaadin.flow.spring.security.AuthenticationContext;
import jakarta.annotation.security.PermitAll;

import static com.vaadin.flow.theme.lumo.LumoUtility.*;
Expand All @@ -24,7 +27,12 @@
@PermitAll // When security is enabled, allow all authenticated users
public final class MainLayout extends AppLayout {

MainLayout() {
private final CurrentUser currentUser;
private final AuthenticationContext authenticationContext;

MainLayout(CurrentUser currentUser, AuthenticationContext authenticationContext) {
this.currentUser = currentUser;
this.authenticationContext = authenticationContext;
setPrimarySection(Section.DRAWER);
addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu());
}
Expand Down Expand Up @@ -58,8 +66,9 @@ private SideNavItem createSideNavItem(MenuEntry menuEntry) {
}

private Component createUserMenu() {
// TODO Replace with real user information and actions
var avatar = new Avatar("John Smith");
var user = currentUser.require();

var avatar = new Avatar(user.getFullName(), user.getPictureUrl());
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
avatar.addClassNames(Margin.Right.SMALL);
avatar.setColorIndex(5);
Expand All @@ -69,10 +78,13 @@ private Component createUserMenu() {
userMenu.addClassNames(Margin.MEDIUM);

var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add("John Smith");
userMenuItem.getSubMenu().addItem("View Profile").setEnabled(false);
userMenuItem.getSubMenu().addItem("Manage Settings").setEnabled(false);
userMenuItem.getSubMenu().addItem("Logout").setEnabled(false);
userMenuItem.add(user.getFullName());
if (user.getProfileUrl() != null) {
userMenuItem.getSubMenu().addItem("View Profile",
event -> UI.getCurrent().getPage().open(user.getProfileUrl()));
}
// TODO Add additional items to the user menu if needed
userMenuItem.getSubMenu().addItem("Logout", event -> authenticationContext.logout());

return userMenu;
}
Expand Down
Loading