Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion assembly/src/assembly/empty-skeleton.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@
<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>
<exclude>src/test/java/com/example/application/ArchitectureTest.java</exclude>
<exclude>src/test/java/com/example/application/taskmanagement/**</exclude>
</excludes>
</fileSet>
</fileSets>
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
57 changes: 53 additions & 4 deletions walking-skeleton/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,20 @@
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>
<dependency>
<!-- Remove if you are not going to use Control Center -->
<groupId>com.vaadin</groupId>
<artifactId>control-center-starter</artifactId>
</dependency>

<!-- Persistence -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<!-- Replace with the database you will be using in production -->
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

Expand All @@ -86,6 +90,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,11 +106,21 @@
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
Expand All @@ -117,6 +135,7 @@
</dependencies>

<build>
<!-- Change the default goal to spring-boot:test-run to start the application with Testcontainers and PostgreSQL -->
<defaultGoal>spring-boot:run</defaultGoal>
<plugins>
<plugin>
Expand Down Expand Up @@ -171,11 +190,41 @@
</build>

<profiles>
<profile>
<!--
To avoid problems, you should use the same database engine in development and in production. Docker,
Testcontainers and service connections make this easy to do. However, this requires you to install and
run Docker on your development machine.

This Maven profile makes it possible to start the skeleton quickly, using an H2 in-memory database and
does not require installing Docker. It does, however, still use Testcontainers and PostgreSQL for
integration tests.

If you have Docker installed and running on your machine, you can delete this profile. When you do this,
you should also change the default goal to `spring-boot:test-run` (at the beginning of the <build> section).
-->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<id>h2-local-development</id>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
<profile>
<id>production</id>
<dependencies>
<!-- Exclude development dependencies from production -->
<dependency>
<!-- Exclude development dependencies from production -->
<groupId>com.vaadin</groupId>
<artifactId>vaadin-core</artifactId>
<exclusions>
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;
1 change: 1 addition & 0 deletions walking-skeleton/src/main/frontend/views/@index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const config: ViewConfig = {
menu: {
exclude: true,
},
loginRequired: true,
};

export default function MainView() {
Expand Down
38 changes: 29 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,9 @@ 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';

function Header() {
// TODO Replace with real application logo and name
Expand Down Expand Up @@ -41,22 +42,41 @@ 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]);

if (!state.user) {
return (
<span {...{ theme: 'badge error' }} slot="drawer">
Not logged in
</span>
);
}

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 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
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.example.application.security;

/**
* Constants for application role names used throughout the security system.
* <p>
* This class centralizes the definition of all role names used in the application, ensuring consistency across security
* configurations, annotations, and tests. Using these constants instead of string literals helps prevent typos and
* makes role management more maintainable.
* </p>
* <p>
* These role names are used in various contexts:
* <ul>
* <li>Security method annotations: {@code @PreAuthorize("hasRole('" + AppRoles.ADMIN + "')")}</li>
* <li>Security configuration and access control rules</li>
* </ul>
* </p>
* <p>
* <strong>Customization:</strong> Modify or add role constants in this class to match your application's security
* requirements. Consider the principle of least privilege when defining roles and their associated permissions.
* </p>
* <p>
* Example usage: <!-- spotless:off -->
* <pre>
* {@code
* // In security annotations
* @PreAuthorize("hasRole('" + AppRoles.ADMIN + "')")
* public void adminOnlyMethod() { ... }
*
* @Route
* @RolesAllowed(AppRoles.ADMIN)
* public class AdminView extends Main { ... }
* }
* </pre>
* <!-- spotless:on -->
* </p>
*
* @see org.springframework.security.access.prepost.PreAuthorize
*/
public final class AppRoles {

/**
* Private constructor to prevent instantiation of this utility class.
*/
private AppRoles() {
}

/**
* Role for administrative users with elevated privileges.
* <p>
* Users with this role typically have access to administrative functions such as user management, system
* configuration, and sensitive operations. Use this role sparingly and only for users who require administrative
* access.
* </p>
*/
public static final String ADMIN = "ADMIN";

/**
* Role for standard application users.
* <p>
* This is the default role for regular users of the application. Users with this role have access to standard
* application features but not administrative functions.
* </p>
*/
public static final String USER = "USER";
}
Loading