diff --git a/build.gradle.kts b/build.gradle.kts index 28f0fca..89dfaaa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/frontend/styles/header.css b/frontend/styles/header.css new file mode 100644 index 0000000..092f893 --- /dev/null +++ b/frontend/styles/header.css @@ -0,0 +1,6 @@ +.header-logo { + width: 100%; + height: 40px; + position: absolute; + align-self: center; +} \ No newline at end of file diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index 4edcbd3..4902ee8 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/java/de/grimsi/gameyfin/layouts/MainLayout.java b/src/main/java/de/grimsi/gameyfin/layouts/MainLayout.java new file mode 100644 index 0000000..a156870 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/layouts/MainLayout.java @@ -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> 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; + } +} \ No newline at end of file diff --git a/src/main/java/de/grimsi/gameyfin/layouts/SetupLayout.java b/src/main/java/de/grimsi/gameyfin/layouts/SetupLayout.java new file mode 100644 index 0000000..3f79851 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/layouts/SetupLayout.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/de/grimsi/gameyfin/resources/PublicResources.java b/src/main/java/de/grimsi/gameyfin/resources/PublicResources.java new file mode 100644 index 0000000..f5d1f8e --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/resources/PublicResources.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/de/grimsi/gameyfin/services/ThemeService.java b/src/main/java/de/grimsi/gameyfin/services/ThemeService.java new file mode 100644 index 0000000..bea1c6f --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/services/ThemeService.java @@ -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"); + } +} \ No newline at end of file diff --git a/src/main/java/de/grimsi/gameyfin/views/LoginView.java b/src/main/java/de/grimsi/gameyfin/views/LoginView.java new file mode 100644 index 0000000..4932f32 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/views/LoginView.java @@ -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); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/grimsi/gameyfin/views/MainView.java b/src/main/java/de/grimsi/gameyfin/views/MainView.java new file mode 100644 index 0000000..7f6274f --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/views/MainView.java @@ -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")); + } +} \ No newline at end of file diff --git a/src/main/java/de/grimsi/gameyfin/views/SetupView.java b/src/main/java/de/grimsi/gameyfin/views/SetupView.java new file mode 100644 index 0000000..3f22ba2 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/views/SetupView.java @@ -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); + } +} + + diff --git a/src/main/kotlin/de/grimsi/gameyfin/GameyfinApplication.kt b/src/main/kotlin/de/grimsi/gameyfin/GameyfinApplication.kt index 0c15054..baa1b18 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/GameyfinApplication.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/GameyfinApplication.kt @@ -8,5 +8,5 @@ import org.springframework.boot.runApplication class GameyfinApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt b/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt index 09d5891..d75a4fa 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt @@ -1,6 +1,7 @@ package de.grimsi.gameyfin.config enum class Roles(val roleName: String) { + SUPERADMIN("ROLE_SUPERADMIN"), ADMIN("ROLE_ADMIN"), USER("ROLE_USER") } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/config/SecurityConfiguration.kt b/src/main/kotlin/de/grimsi/gameyfin/config/SecurityConfiguration.kt index 2d09198..0b5e877 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/config/SecurityConfiguration.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/config/SecurityConfiguration.kt @@ -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 diff --git a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt index d45b57a..b2ee96a 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt @@ -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" } diff --git a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt index e8d9615..9b6d675 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt @@ -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 } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/MainLayout.kt b/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/MainLayout.kt deleted file mode 100644 index 8cb2667..0000000 --- a/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/MainLayout.kt +++ /dev/null @@ -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) -> Unit - ): HorizontalLayout { - return menuItem(icon, title).apply { - onLeftClick(action) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/SetupLayout.kt b/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/SetupLayout.kt deleted file mode 100644 index 7a06256..0000000 --- a/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/SetupLayout.kt +++ /dev/null @@ -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) - } - } - } - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/ui/resources/PublicResources.kt b/src/main/kotlin/de/grimsi/gameyfin/ui/resources/PublicResources.kt deleted file mode 100644 index 622b71a..0000000 --- a/src/main/kotlin/de/grimsi/gameyfin/ui/resources/PublicResources.kt +++ /dev/null @@ -1,5 +0,0 @@ -package de.grimsi.gameyfin.ui.resources - -enum class PublicResources(val path: String) { - GAMEYFIN_LOGO("public/images/Logo.svg") -} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/ui/services/ThemeService.kt b/src/main/kotlin/de/grimsi/gameyfin/ui/services/ThemeService.kt deleted file mode 100644 index 2118ae4..0000000 --- a/src/main/kotlin/de/grimsi/gameyfin/ui/services/ThemeService.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/ui/views/LoginView.kt b/src/main/kotlin/de/grimsi/gameyfin/ui/views/LoginView.kt deleted file mode 100644 index ed45101..0000000 --- a/src/main/kotlin/de/grimsi/gameyfin/ui/views/LoginView.kt +++ /dev/null @@ -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" - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/ui/views/MainView.kt b/src/main/kotlin/de/grimsi/gameyfin/ui/views/MainView.kt deleted file mode 100644 index c2ba575..0000000 --- a/src/main/kotlin/de/grimsi/gameyfin/ui/views/MainView.kt +++ /dev/null @@ -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" } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/ui/views/SetupView.kt b/src/main/kotlin/de/grimsi/gameyfin/ui/views/SetupView.kt deleted file mode 100644 index 97ed192..0000000 --- a/src/main/kotlin/de/grimsi/gameyfin/ui/views/SetupView.kt +++ /dev/null @@ -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") - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt b/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt new file mode 100644 index 0000000..683c910 --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt b/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt index 0e2ab65..a7b66a8 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt @@ -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): List { return roles.map { r -> SimpleGrantedAuthority(r.rolename) } } diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt b/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt index 83cd716..b2a802a 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt @@ -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 = emptyList() ) \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/util/UserDetailsExtensions.kt b/src/main/kotlin/de/grimsi/gameyfin/users/util/UserDetailsExtensions.kt index c5cefa0..a07f585 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/util/UserDetailsExtensions.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/util/UserDetailsExtensions.kt @@ -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) } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ab598fa..6cd09ef 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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: