mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-17 08:15:44 +00:00
Implement user management
Implement dark mode for UI
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
enum class Roles(val roleName: String) {
|
||||
ADMIN("ROLE_ADMIN"),
|
||||
USER("ROLE_USER")
|
||||
}
|
||||
+5
-20
@@ -1,4 +1,4 @@
|
||||
package de.grimsi.gameyfin.security
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import com.vaadin.flow.spring.security.VaadinWebSecurity
|
||||
import de.grimsi.gameyfin.ui.views.LoginView
|
||||
@@ -8,10 +8,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.provisioning.UserDetailsManager
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
|
||||
|
||||
|
||||
@@ -39,21 +37,8 @@ class SecurityConfiguration : VaadinWebSecurity() {
|
||||
super.configure(web)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Just for testing
|
||||
*/
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsManager {
|
||||
val user: UserDetails =
|
||||
User.withUsername("user")
|
||||
.password("{noop}user")
|
||||
.roles("USER")
|
||||
.build()
|
||||
val admin: UserDetails =
|
||||
User.withUsername("admin")
|
||||
.password("{noop}admin")
|
||||
.roles("ADMIN")
|
||||
.build()
|
||||
return InMemoryUserDetailsManager(user, admin)
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return BCryptPasswordEncoder()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package de.grimsi.gameyfin.setup
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
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
|
||||
class SetupDataLoader(
|
||||
private val userRepository: UserRepository,
|
||||
private val roleRepository: RoleRepository,
|
||||
private val passwordEncoder: PasswordEncoder
|
||||
) {
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Transactional
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun setupRoles() {
|
||||
|
||||
createRoleIfNotFound("ROLE_ADMIN")
|
||||
createRoleIfNotFound("ROLE_USER")
|
||||
|
||||
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))
|
||||
|
||||
log.info { "Role setup completed." }
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createRoleIfNotFound(name: String): Role {
|
||||
log.info { "Creating role $name" }
|
||||
|
||||
var role: Role? = roleRepository.findByRolename(name)
|
||||
|
||||
if (role == null) {
|
||||
role = Role(name)
|
||||
roleRepository.save(role)
|
||||
}
|
||||
return role
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.grimsi.gameyfin.setup
|
||||
|
||||
import jakarta.servlet.*
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.http.HttpStatus
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
//@Order(1)
|
||||
//@Component
|
||||
class SetupFilter(
|
||||
private val setupService: SetupService
|
||||
) : Filter {
|
||||
|
||||
@Throws(ServletException::class, IOException::class)
|
||||
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) {
|
||||
val req = servletRequest as HttpServletRequest
|
||||
val res = servletResponse as HttpServletResponse
|
||||
|
||||
val isSetupUri = req.requestURI.contains("/v1/setup")
|
||||
|
||||
if (setupService.isSetupCompleted() && !isSetupUri ||
|
||||
!setupService.isSetupCompleted() && isSetupUri
|
||||
) {
|
||||
filterChain.doFilter(req, res)
|
||||
} else {
|
||||
res.status = HttpStatus.FORBIDDEN.value()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.grimsi.gameyfin.setup
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class SetupService {
|
||||
|
||||
fun isSetupCompleted() : Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -4,26 +4,35 @@ 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.applayout.AppLayout
|
||||
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.security.isAdmin
|
||||
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
|
||||
|
||||
|
||||
class MainLayout(@field:Transient private val authContext: AuthenticationContext) : KComposite(), RouterLayout {
|
||||
|
||||
|
||||
private val appLayout: AppLayout
|
||||
@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()
|
||||
|
||||
appLayout = ui {
|
||||
ui {
|
||||
|
||||
appLayout {
|
||||
navbar {
|
||||
@@ -46,12 +55,30 @@ class MainLayout(@field:Transient private val authContext: AuthenticationContext
|
||||
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"))
|
||||
if (user.isAdmin()) item(menuItem(FontAwesome.Solid.COG, "Administration"))
|
||||
item(menuItem(FontAwesome.Solid.QUESTION_CIRCLE, "Help"))
|
||||
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() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ 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.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
|
||||
@@ -15,7 +13,7 @@ import de.grimsi.gameyfin.ui.resources.PublicResources
|
||||
@Route("login")
|
||||
@PageTitle("Login")
|
||||
@AnonymousAllowed
|
||||
class LoginView : VerticalLayout(), BeforeEnterObserver {
|
||||
class LoginView : VerticalLayout() {
|
||||
|
||||
private var login: LoginForm
|
||||
|
||||
@@ -35,17 +33,4 @@ class LoginView : VerticalLayout(), BeforeEnterObserver {
|
||||
action = "login"
|
||||
}
|
||||
}
|
||||
|
||||
override fun beforeEnter(event: BeforeEnterEvent?) {
|
||||
if (event != null) {
|
||||
if (event.location
|
||||
.queryParameters
|
||||
.parameters
|
||||
.containsKey("error")
|
||||
) {
|
||||
login.isError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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,37 @@
|
||||
package de.grimsi.gameyfin.users
|
||||
|
||||
import de.grimsi.gameyfin.users.entities.Role
|
||||
import de.grimsi.gameyfin.users.persistence.UserRepository
|
||||
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.stereotype.Service
|
||||
|
||||
|
||||
@Service
|
||||
class UserService(
|
||||
private val userRepository: UserRepository
|
||||
) : UserDetailsService {
|
||||
|
||||
override fun loadUserByUsername(username: String): UserDetails {
|
||||
val user =
|
||||
userRepository.findByUsername(username) ?: throw UsernameNotFoundException("Unknown user '$username'")
|
||||
|
||||
return User(
|
||||
user.username,
|
||||
user.password,
|
||||
user.enabled,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
getAuthorities(user.roles)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAuthorities(roles: Collection<Role>): List<GrantedAuthority> {
|
||||
return roles.map { r -> SimpleGrantedAuthority(r.rolename) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.grimsi.gameyfin.users.entities
|
||||
|
||||
import jakarta.annotation.Nullable
|
||||
import jakarta.persistence.Embeddable
|
||||
import org.springframework.content.commons.annotations.ContentId
|
||||
import org.springframework.content.commons.annotations.ContentLength
|
||||
import org.springframework.content.commons.annotations.MimeType
|
||||
|
||||
|
||||
@Embeddable
|
||||
class Avatar {
|
||||
@ContentId
|
||||
@Nullable
|
||||
var contentId: String? = null
|
||||
|
||||
@ContentLength
|
||||
@Nullable
|
||||
var contentLength: Long? = null
|
||||
|
||||
@MimeType
|
||||
@Nullable
|
||||
var mimeType: String? = null
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.grimsi.gameyfin.users.entities
|
||||
|
||||
import jakarta.persistence.*
|
||||
import jakarta.validation.constraints.NotNull
|
||||
|
||||
|
||||
@Entity
|
||||
class Role(
|
||||
@NotNull
|
||||
var rolename: String,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
var id: Long? = null,
|
||||
|
||||
@ManyToMany(mappedBy = "roles")
|
||||
var users: Collection<User> = emptyList()
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.grimsi.gameyfin.users.entities
|
||||
|
||||
import jakarta.annotation.Nullable
|
||||
import jakarta.persistence.*
|
||||
import jakarta.validation.constraints.NotNull
|
||||
|
||||
|
||||
@Entity
|
||||
class User(
|
||||
@NotNull
|
||||
var username: String,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
var id: Long? = null,
|
||||
|
||||
@NotNull
|
||||
var password: String? = null,
|
||||
|
||||
@Nullable
|
||||
var email: String? = null,
|
||||
|
||||
var enabled: Boolean = true,
|
||||
|
||||
@Embedded
|
||||
@Nullable
|
||||
var avatar: Avatar? = null,
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(
|
||||
name = "users_roles",
|
||||
joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "role_id", referencedColumnName = "id")]
|
||||
)
|
||||
var roles: Collection<Role> = emptyList()
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.grimsi.gameyfin.users.persistence
|
||||
|
||||
import de.grimsi.gameyfin.users.entities.Avatar
|
||||
import org.springframework.content.commons.store.ContentStore
|
||||
|
||||
interface AvatarContentStore : ContentStore<Avatar, String>
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.grimsi.gameyfin.users.persistence
|
||||
|
||||
import de.grimsi.gameyfin.users.entities.Role
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface RoleRepository : JpaRepository<Role, Long> {
|
||||
fun findByRolename(roleName: String): Role?
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.grimsi.gameyfin.users.persistence
|
||||
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface UserRepository : JpaRepository<User, Long> {
|
||||
fun findByUsername(userName: String): User?
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.grimsi.gameyfin.security
|
||||
package de.grimsi.gameyfin.users.util
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
|
||||
@@ -2,4 +2,10 @@ vaadin.whitelisted-packages:
|
||||
- com.vaadin
|
||||
- org.vaadin
|
||||
- dev.hilla
|
||||
- com.flowingcode
|
||||
- com.flowingcode
|
||||
|
||||
spring:
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
globally_quoted_identifiers: true
|
||||
Reference in New Issue
Block a user