mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Experimenting with Vaadin
This commit is contained in:
+11
-2
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user