Experimenting with Vaadin

This commit is contained in:
grimsi
2024-02-20 16:17:17 +01:00
parent 64280579f2
commit 73457aad0b
27 changed files with 366 additions and 315 deletions
+11 -2
View File
@@ -7,6 +7,8 @@ plugins {
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22"
kotlin("plugin.jpa") version "1.9.22"
id("io.freefair.lombok") version "8.4"
java
}
allOpen {
@@ -27,6 +29,7 @@ configurations {
}
repositories {
mavenLocal()
mavenCentral()
maven {
name = "Vaadin Addons"
@@ -55,10 +58,13 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("com.github.mvysny.karibudsl:karibu-dsl-v23:2.1.0")
implementation("com.github.mvysny.karibu-tools:karibu-tools-23:0.19")
implementation("com.flowingcode.addons:font-awesome-iron-iconset:5.2.2")
implementation("com.vaadin.componentfactory:autocomplete:24.1.7")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// Vaadin Add-Ons
implementation("com.flowingcode.addons:font-awesome-iron-iconset:5.2.2")
implementation("in.virit:viritin:2.7.0")
implementation("io.sunshower.aire:aire-wizard:1.0.17.Final")
// Development
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
@@ -66,6 +72,9 @@ dependencies {
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
// Fix compilation error
compileOnly("com.github.spotbugs:spotbugs-annotations:4.8.3")
}
dependencyManagement {
+6
View File
@@ -0,0 +1,6 @@
.header-logo {
width: 100%;
height: 40px;
position: absolute;
align-self: center;
}
Binary file not shown.
@@ -0,0 +1,90 @@
package de.grimsi.gameyfin.layouts;
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome;
import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.ComponentEventListener;
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.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.contextmenu.MenuItem;
import com.vaadin.flow.component.contextmenu.SubMenu;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.spring.security.AuthenticationContext;
import de.grimsi.gameyfin.resources.PublicResources;
import de.grimsi.gameyfin.services.ThemeService;
import de.grimsi.gameyfin.setup.SetupService;
import de.grimsi.gameyfin.views.SetupView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.vaadin.firitin.util.style.LumoProps;
import static de.grimsi.gameyfin.users.util.Utils.isAdmin;
@JsModule("./scripts/prefers-color-scheme.js")
@CssImport("./styles/header.css")
public class MainLayout extends AppLayout {
public MainLayout(AuthenticationContext authContext,
@Autowired SetupService setupService,
@Autowired ThemeService themeService) {
if (!setupService.isSetupCompleted()) {
UI.getCurrent().navigate(SetupView.class);
UI.getCurrent().close();
}
UserDetails user = authContext.getAuthenticatedUser(UserDetails.class).get();
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin Logo");
logo.addClassName("header-logo");
Button toggleTheme = new Button(FontAwesome.Solid.CIRCLE_HALF_STROKE.create());
toggleTheme.addThemeVariants(ButtonVariant.LUMO_ICON);
toggleTheme.addClickListener(listener -> themeService.toggleTheme());
Avatar avatar = new Avatar(user.getUsername());
avatar.setAbbreviation(user.getUsername().substring(0, 2).toUpperCase());
avatar.setColorIndex(user.getUsername().chars().map(i -> i % 6).findFirst().getAsInt());
MenuBar menu = new MenuBar();
menu.addThemeVariants(MenuBarVariant.LUMO_ICON);
MenuItem item = menu.addItem(avatar);
SubMenu subMenu = item.getSubMenu();
subMenu.addItem(menuItem(FontAwesome.Solid.USER, "Profile", l -> Notification.show("Profile")));
if (isAdmin(user)) {
subMenu.addItem(menuItem(FontAwesome.Solid.COG, "Administration", l -> Notification.show("Administration")));
}
subMenu.addItem(menuItem(FontAwesome.Solid.QUESTION_CIRCLE, "Help", l -> Notification.show("Help")));
subMenu.addItem(menuItem(FontAwesome.Solid.SIGN_OUT, "Sign out", l -> authContext.logout()));
HorizontalLayout horizontalLayout = new HorizontalLayout();
horizontalLayout.setAlignItems(FlexComponent.Alignment.END);
horizontalLayout.add(logo, toggleTheme, menu);
addToNavbar(horizontalLayout);
}
private HorizontalLayout menuItem(FontAwesome.Solid icon, String title, ComponentEventListener<ClickEvent<HorizontalLayout>> listener) {
FontAwesome.Solid.Icon i = icon.create();
i.setSize(LumoProps.ICON_SIZE_S.var());
HorizontalLayout h = new HorizontalLayout();
h.setAlignItems(FlexComponent.Alignment.CENTER);
h.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
h.add(i);
h.add(title);
h.addClickListener(listener);
return h;
}
}
@@ -0,0 +1,33 @@
package de.grimsi.gameyfin.layouts;
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import de.grimsi.gameyfin.resources.PublicResources;
import de.grimsi.gameyfin.services.ThemeService;
import org.springframework.beans.factory.annotation.Autowired;
@CssImport("./styles/header.css")
public class SetupLayout extends AppLayout {
public SetupLayout(@Autowired ThemeService themeService) {
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin Logo");
logo.addClassName("header-logo");
Button toggleTheme = new Button(FontAwesome.Solid.CIRCLE_HALF_STROKE.create());
toggleTheme.addThemeVariants(ButtonVariant.LUMO_ICON);
toggleTheme.addClickListener(listener -> themeService.toggleTheme());
HorizontalLayout horizontalLayout = new HorizontalLayout();
horizontalLayout.setWidthFull();
horizontalLayout.setAlignSelf(FlexComponent.Alignment.END);
horizontalLayout.add(logo, toggleTheme);
addToNavbar(horizontalLayout);
}
}
@@ -0,0 +1,11 @@
package de.grimsi.gameyfin.resources;
public enum PublicResources {
GAMEYFIN_LOGO("public/images/Logo.svg");
public final String path;
PublicResources(String path) {
this.path = path;
}
}
@@ -0,0 +1,11 @@
package de.grimsi.gameyfin.services;
import com.vaadin.flow.component.notification.Notification;
import org.springframework.stereotype.Service;
@Service
public class ThemeService {
public void toggleTheme() {
Notification.show("Not implemented");
}
}
@@ -0,0 +1,48 @@
package de.grimsi.gameyfin.views;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Image;
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;
import de.grimsi.gameyfin.resources.PublicResources;
import de.grimsi.gameyfin.setup.SetupService;
import org.springframework.beans.factory.annotation.Autowired;
@Route("login")
@PageTitle("Login")
@AnonymousAllowed
public class LoginView extends VerticalLayout implements BeforeEnterObserver {
private final LoginForm login = new LoginForm();
public LoginView(@Autowired SetupService setupService) {
if (!setupService.isSetupCompleted()) {
UI.getCurrent().navigate(SetupView.class);
UI.getCurrent().close();
}
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin");
logo.setHeight("100px");
login.setAction("login");
add(logo);
add(login);
}
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
if (beforeEnterEvent.getLocation()
.getQueryParameters()
.getParameters()
.containsKey("error")
) {
login.setError(true);
}
}
}
@@ -0,0 +1,21 @@
package de.grimsi.gameyfin.views;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Pre;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.grimsi.gameyfin.layouts.MainLayout;
import jakarta.annotation.security.PermitAll;
@Route(value = "", layout = MainLayout.class)
@PageTitle("Gameyfin")
@PermitAll
public class MainView extends VerticalLayout {
public MainView() {
add(new H1("Gameyfin main page"));
add(new Pre("Work in progress"));
}
}
@@ -0,0 +1,46 @@
package de.grimsi.gameyfin.views;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.grimsi.gameyfin.layouts.SetupLayout;
import de.grimsi.gameyfin.setup.SetupService;
import org.springframework.beans.factory.annotation.Autowired;
@Route(value = "/setup", layout = SetupLayout.class)
@PageTitle("Setup")
@AnonymousAllowed
public class SetupView extends VerticalLayout {
public SetupView(@Autowired SetupService setupService) {
if (setupService.isSetupCompleted()) {
UI.getCurrent().navigate(LoginView.class);
UI.getCurrent().close();
}
setWidthFull();
setAlignItems(Alignment.CENTER);
add(new Text("Looks like it's your first time starting Gameyfin. Let's continue setting up your very own instance 🙂"));
TextField username = new TextField("Username");
username.focus();
PasswordField passwordField = new PasswordField("Password");
PasswordField passwordFieldRepeat = new PasswordField("Password (repeated)");
FormLayout form = new FormLayout();
form.add(new Text("Let's start with creating a super admin account. This account will have full permissions."));
form.add(username, passwordField, passwordFieldRepeat);
add(form);
}
}
@@ -8,5 +8,5 @@ import org.springframework.boot.runApplication
class GameyfinApplication
fun main(args: Array<String>) {
runApplication<GameyfinApplication>(*args)
runApplication<GameyfinApplication>(*args)
}
@@ -1,6 +1,7 @@
package de.grimsi.gameyfin.config
enum class Roles(val roleName: String) {
SUPERADMIN("ROLE_SUPERADMIN"),
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER")
}
@@ -1,7 +1,7 @@
package de.grimsi.gameyfin.config
import com.vaadin.flow.spring.security.VaadinWebSecurity
import de.grimsi.gameyfin.ui.views.LoginView
import de.grimsi.gameyfin.views.LoginView
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
@@ -1,50 +1,55 @@
package de.grimsi.gameyfin.setup
import de.grimsi.gameyfin.config.Roles
import de.grimsi.gameyfin.users.UserService
import de.grimsi.gameyfin.users.entities.Role
import de.grimsi.gameyfin.users.entities.User
import de.grimsi.gameyfin.users.persistence.RoleRepository
import de.grimsi.gameyfin.users.persistence.UserRepository
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.transaction.Transactional
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.event.EventListener
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
@Service
@Transactional
class SetupDataLoader(
private val userRepository: UserRepository,
private val roleRepository: RoleRepository,
private val passwordEncoder: PasswordEncoder
private val userService: UserService
) {
private val log = KotlinLogging.logger {}
@Transactional
@EventListener(ApplicationReadyEvent::class)
fun initialSetup() {
log.info { "Looks like this is the first time your're starting Gameyfin." }
log.info { "We will now set up some data..." }
setupRoles()
//setupUser()
log.info { "Setup completed..." }
}
fun setupUser() {
val superadmin = User("admin")
superadmin.password = "admin"
superadmin.roles = listOf(roleRepository.findByRolename(Roles.SUPERADMIN.roleName)!!)
userService.registerUser(superadmin)
}
fun setupRoles() {
createRoleIfNotFound("ROLE_ADMIN")
createRoleIfNotFound("ROLE_USER")
log.info { "Setting up roles..." }
val adminRole: Role = roleRepository.findByRolename(Roles.ADMIN.roleName)!!
val userRole: Role = roleRepository.findByRolename(Roles.USER.roleName)!!
val admin = User("admin")
admin.password = passwordEncoder.encode("admin")
admin.roles = listOf(adminRole)
val user = User("user")
user.password = passwordEncoder.encode("user")
user.roles = listOf(userRole)
userRepository.saveAll(listOf(admin, user))
createRoleIfNotFound(Roles.SUPERADMIN.roleName)
createRoleIfNotFound(Roles.ADMIN.roleName)
createRoleIfNotFound(Roles.USER.roleName)
log.info { "Role setup completed." }
}
@Transactional
fun createRoleIfNotFound(name: String): Role {
log.info { "Creating role $name" }
@@ -1,11 +1,20 @@
package de.grimsi.gameyfin.setup
import de.grimsi.gameyfin.config.Roles
import de.grimsi.gameyfin.users.RoleService
import org.springframework.stereotype.Service
@Service
class SetupService {
class SetupService(
private val roleService: RoleService
) {
fun isSetupCompleted() : Boolean {
return false
/**
* Checks if the minimal requirements to run Gameyfin are fulfilled
* Currently these are:
* 1. At least one user with "Super Admin" role
*/
fun isSetupCompleted(): Boolean {
return roleService.getUserCountForRole(Roles.SUPERADMIN) > 0
}
}
@@ -1,114 +0,0 @@
package de.grimsi.gameyfin.ui.layouts
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome
import com.github.mvysny.karibudsl.v10.*
import com.github.mvysny.kaributools.tooltip
import com.vaadin.flow.component.ClickEvent
import com.vaadin.flow.component.UI
import com.vaadin.flow.component.dependency.JsModule
import com.vaadin.flow.component.menubar.MenuBarVariant
import com.vaadin.flow.component.notification.Notification
import com.vaadin.flow.component.orderedlayout.FlexComponent
import com.vaadin.flow.component.orderedlayout.HorizontalLayout
import com.vaadin.flow.router.RouterLayout
import com.vaadin.flow.spring.security.AuthenticationContext
import de.grimsi.gameyfin.setup.SetupService
import de.grimsi.gameyfin.ui.resources.PublicResources
import de.grimsi.gameyfin.ui.services.ThemeService
import de.grimsi.gameyfin.ui.views.SetupView
import de.grimsi.gameyfin.users.util.isAdmin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.userdetails.UserDetails
@JsModule("./scripts/prefers-color-scheme.js")
class MainLayout(
@field:Transient private val authContext: AuthenticationContext,
@Autowired private val setupService: SetupService,
@Autowired private val themeService: ThemeService
) : KComposite(), RouterLayout {
init {
if (!setupService.isSetupCompleted()) UI.getCurrent().navigate(SetupView::class.java)
val user = authContext.getAuthenticatedUser(UserDetails::class.java).get()
ui {
appLayout {
navbar {
flexLayout {
setWidthFull()
alignItems = FlexComponent.Alignment.CENTER
image(PublicResources.GAMEYFIN_LOGO.path) {
setWidthFull()
height = "40px"
className = "header-logo"
}
horizontalLayout {
alignItems = FlexComponent.Alignment.CENTER
val a = avatar(user.username) {
tooltip = user.username
abbreviation = user.username.take(2).uppercase()
colorIndex = user.username[0].code.toByte().mod(6)
}
val toggleDarkModeIcon =
FontAwesome.Solid.CIRCLE_HALF_STROKE.create { _ -> themeService.toggleTheme() }
iconButton(toggleDarkModeIcon)
menuBar {
addThemeVariants(MenuBarVariant.LUMO_ICON)
item(a) {
item(
menuItem(
FontAwesome.Solid.USER,
"Profile"
) { _ -> Notification.show("Profile") })
if (user.isAdmin()) {
item(menuItem(FontAwesome.Solid.COG, "Administration") { _ ->
Notification.show(
"Administration"
)
})
}
item(
menuItem(
FontAwesome.Solid.QUESTION_CIRCLE,
"Help"
) { _ -> Notification.show("Help") })
item(menuItem(FontAwesome.Solid.SIGN_OUT, "Sign out") { _ -> authContext.logout() })
}
}
}
}
}
}
}
}
private fun menuItem(icon: FontAwesome.Solid, title: String): HorizontalLayout {
return HorizontalLayout().apply {
alignItems = FlexComponent.Alignment.CENTER
justifyContentMode = FlexComponent.JustifyContentMode.START
val faIcon = icon.create()
faIcon.setSize("var(--lumo-icon-size-s)")
add(faIcon)
text(title)
}
}
private fun menuItem(
icon: FontAwesome.Solid,
title: String,
action: (ClickEvent<HorizontalLayout>) -> Unit
): HorizontalLayout {
return menuItem(icon, title).apply {
onLeftClick(action)
}
}
}
@@ -1,47 +0,0 @@
package de.grimsi.gameyfin.ui.layouts
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome
import com.github.mvysny.karibudsl.v10.*
import com.vaadin.flow.component.UI
import com.vaadin.flow.component.orderedlayout.FlexComponent
import com.vaadin.flow.router.RouterLayout
import de.grimsi.gameyfin.setup.SetupService
import de.grimsi.gameyfin.ui.resources.PublicResources
import de.grimsi.gameyfin.ui.services.ThemeService
import de.grimsi.gameyfin.ui.views.LoginView
import org.springframework.beans.factory.annotation.Autowired
class SetupLayout(
@Autowired private val setupService: SetupService,
@Autowired private val themeService: ThemeService
) : KComposite(), RouterLayout {
init {
if (setupService.isSetupCompleted()) UI.getCurrent().navigate(LoginView::class.java)
ui {
appLayout {
navbar {
flexLayout {
setWidthFull()
alignItems = FlexComponent.Alignment.CENTER
image(PublicResources.GAMEYFIN_LOGO.path) {
setWidthFull()
height = "40px"
className = "header-logo"
}
horizontalLayout {
alignItems = FlexComponent.Alignment.CENTER
val toggleDarkModeIcon =
FontAwesome.Solid.CIRCLE_HALF_STROKE.create { _ -> themeService.toggleTheme() }
iconButton(toggleDarkModeIcon)
}
}
}
}
}
}
}
@@ -1,5 +0,0 @@
package de.grimsi.gameyfin.ui.resources
enum class PublicResources(val path: String) {
GAMEYFIN_LOGO("public/images/Logo.svg")
}
@@ -1,25 +0,0 @@
package de.grimsi.gameyfin.ui.services
import com.vaadin.flow.component.UI
import com.vaadin.flow.theme.lumo.Lumo
import org.springframework.stereotype.Service
@Service
class ThemeService {
fun isDarkModeActive(): Boolean {
val js = "document.documentElement.getAttribute('theme')"
return UI.getCurrent().element.executeJs(js).toCompletableFuture().get().asString() == "dark"
}
fun toggleTheme() {
setTheme(!isDarkModeActive())
}
fun setTheme(dark: Boolean) {
val js = "document.documentElement.setAttribute('theme', $0)"
UI.getCurrent().element.executeJs(js, if (dark) Lumo.DARK else Lumo.LIGHT)
}
}
@@ -1,36 +0,0 @@
package de.grimsi.gameyfin.ui.views
import com.github.mvysny.karibudsl.v10.image
import com.github.mvysny.karibudsl.v10.loginForm
import com.vaadin.flow.component.login.LoginForm
import com.vaadin.flow.component.orderedlayout.FlexComponent
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.PageTitle
import com.vaadin.flow.router.Route
import com.vaadin.flow.server.auth.AnonymousAllowed
import de.grimsi.gameyfin.ui.resources.PublicResources
@Route("login")
@PageTitle("Login")
@AnonymousAllowed
class LoginView : VerticalLayout() {
private var login: LoginForm
init {
setSizeFull()
justifyContentMode = FlexComponent.JustifyContentMode.CENTER
alignItems = FlexComponent.Alignment.CENTER
image {
height = "100px"
src = PublicResources.GAMEYFIN_LOGO.path
setAlt("Gameyfin")
}
login = loginForm {
addClassName("login-view")
action = "login"
}
}
}
@@ -1,22 +0,0 @@
package de.grimsi.gameyfin.ui.views
import com.github.mvysny.karibudsl.v10.h1
import com.github.mvysny.karibudsl.v10.pre
import com.github.mvysny.karibudsl.v10.verticalLayout
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.Route
import de.grimsi.gameyfin.ui.layouts.MainLayout
import jakarta.annotation.security.PermitAll
@Route("", layout = MainLayout::class)
@PermitAll
class MainView : VerticalLayout() {
init {
verticalLayout {
h1 { text = "Gameyfin main page" }
pre { text = "Work in progress" }
}
}
}
@@ -1,27 +0,0 @@
package de.grimsi.gameyfin.ui.views
import com.github.mvysny.karibudsl.v10.KComposite
import com.github.mvysny.karibudsl.v10.flexLayout
import com.github.mvysny.karibudsl.v10.h1
import com.vaadin.flow.component.orderedlayout.FlexComponent
import com.vaadin.flow.component.orderedlayout.FlexLayout
import com.vaadin.flow.router.Route
import com.vaadin.flow.server.auth.AnonymousAllowed
import de.grimsi.gameyfin.ui.layouts.SetupLayout
@Route("/setup", layout = SetupLayout::class)
@AnonymousAllowed
class SetupView : KComposite() {
init {
ui {
flexLayout {
setWidthFull()
alignItems = FlexComponent.Alignment.CENTER
alignContent = FlexLayout.ContentAlignment.CENTER
h1("Setup View")
}
}
}
}
@@ -0,0 +1,21 @@
package de.grimsi.gameyfin.users
import de.grimsi.gameyfin.config.Roles
import de.grimsi.gameyfin.users.persistence.RoleRepository
import jakarta.transaction.Transactional
import org.springframework.stereotype.Service
@Service
@Transactional
class RoleService(
private val roleRepository: RoleRepository
) {
/**
* @return the number of registered users with a given role
* @return 0 if a role does not exist
*/
fun getUserCountForRole(role: Roles): Int {
val r = roleRepository.findByRolename(role.roleName) ?: return 0
return r.users.size
}
}
@@ -1,26 +1,30 @@
package de.grimsi.gameyfin.users
import de.grimsi.gameyfin.users.entities.Role
import de.grimsi.gameyfin.users.entities.User
import de.grimsi.gameyfin.users.persistence.UserRepository
import jakarta.transaction.Transactional
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
@Service
@Transactional
class UserService(
private val userRepository: UserRepository
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder
) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
val user =
userRepository.findByUsername(username) ?: throw UsernameNotFoundException("Unknown user '$username'")
return User(
return org.springframework.security.core.userdetails.User(
user.username,
user.password,
user.enabled,
@@ -31,6 +35,11 @@ class UserService(
)
}
fun registerUser(user: User): User {
user.password = passwordEncoder.encode(user.password)
return userRepository.save(user)
}
private fun getAuthorities(roles: Collection<Role>): List<GrantedAuthority> {
return roles.map { r -> SimpleGrantedAuthority(r.rolename) }
}
@@ -13,6 +13,6 @@ class Role(
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null,
@ManyToMany(mappedBy = "roles")
@ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER)
var users: Collection<User> = emptyList()
)
@@ -1,11 +1,15 @@
@file:JvmName("Utils")
@file:JvmMultifileClass
package de.grimsi.gameyfin.users.util
import de.grimsi.gameyfin.config.Roles
import org.springframework.security.core.userdetails.UserDetails
fun UserDetails.hasRole(role: String): Boolean {
return this.authorities.map { a -> a.authority }.contains("ROLE_".plus(role))
fun UserDetails.hasRole(role: Roles): Boolean {
return this.authorities.map { a -> a.authority }.contains(role.roleName)
}
fun UserDetails.isAdmin(): Boolean {
return hasRole("ADMIN")
return hasRole(Roles.SUPERADMIN) || hasRole(Roles.ADMIN)
}
+8 -5
View File
@@ -1,8 +1,11 @@
vaadin.whitelisted-packages:
- com.vaadin
- org.vaadin
- dev.hilla
- com.flowingcode
vaadin:
whitelisted-packages:
- com.vaadin
- org.vaadin
- dev.hilla
- com.flowingcode
frontend:
hotdeploy: false
spring:
jpa: