mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Implement dynamic config management
Start implementation of library management
This commit is contained in:
@@ -2,9 +2,11 @@ package de.grimsi.gameyfin
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableTransactionManagement
|
||||
class GameyfinApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
|
||||
@Endpoint
|
||||
@RolesAllowed(Roles.Names.SUPERADMIN, Roles.Names.ADMIN)
|
||||
class ConfigController(
|
||||
private val appConfigService: ConfigService
|
||||
) {
|
||||
|
||||
fun getConfig(key: String): String {
|
||||
return appConfigService.getConfigValue(key)
|
||||
}
|
||||
|
||||
fun setConfig(config: Pair<String, String>) {
|
||||
appConfigService.setConfigValue(config.first, config.second)
|
||||
}
|
||||
|
||||
fun resetConfig(key: String) {
|
||||
appConfigService.resetConfigValue(key)
|
||||
}
|
||||
|
||||
fun deleteConfig(key: String) {
|
||||
appConfigService.deleteConfig(key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
sealed class ConfigProperty<T : Serializable>(val type: KClass<T>, val key: String, val default: T? = null) {
|
||||
|
||||
/** Libraries */
|
||||
// Allow access to game libraries without login
|
||||
data object LibraryAllowPublicAccess :
|
||||
ConfigProperty<Boolean>(Boolean::class, "library.allow-public-access", false)
|
||||
|
||||
// Enable automatic library scanning using file system watchers
|
||||
data object LibraryEnableFilesystemWatcher :
|
||||
ConfigProperty<Boolean>(Boolean::class, "library.scan.enable-filesystem-watcher", true)
|
||||
|
||||
// Enable periodic refresh of video game meta-data and set the schedule (default is once per week)
|
||||
data object LibraryMetadataUpdateEnabled :
|
||||
ConfigProperty<Boolean>(Boolean::class, "library.metadata.update.enabled", true)
|
||||
|
||||
data object LibraryMetadataUpdateSchedule :
|
||||
ConfigProperty<String>(String::class, "library.metadata.update.schedule", "0 0 * * 0")
|
||||
|
||||
/** User management */
|
||||
// Allow new users to sign up by themselves
|
||||
data object UsersAllowNewSignUps : ConfigProperty<Boolean>(Boolean::class, "users.sign-ups.allow", false)
|
||||
|
||||
// If an administrator needs to confirm new sign-ups before they are allowed to log in
|
||||
data object UsersConfirmNewSignUps :
|
||||
ConfigProperty<Boolean>(Boolean::class, "users.sign-ups.confirm", false)
|
||||
|
||||
/** Notifications */
|
||||
// Settings for the mail server used by Gameyfin to send notifications
|
||||
data object NotificationsEmailHost : ConfigProperty<String>(String::class, "notifications.email.host")
|
||||
data object NotificationsEmailPort : ConfigProperty<String>(String::class, "notifications.email.port")
|
||||
data object NotificationsEmailUsername : ConfigProperty<String>(String::class, "notifications.email.username")
|
||||
data object NotificationsEmailPassword : ConfigProperty<String>(String::class, "notifications.email.password")
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import de.grimsi.gameyfin.config.entities.ConfigEntry
|
||||
import de.grimsi.gameyfin.config.persistence.ConfigRepository
|
||||
import jakarta.transaction.Transactional
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.safeCast
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
class ConfigService(
|
||||
private val appConfigRepository: ConfigRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Get the current value of a config property in a type-safe way.
|
||||
* Used internally.
|
||||
*
|
||||
* @param configProperty: The config property containing necessary type information
|
||||
* @return The current value if set or the default value
|
||||
* @throws IllegalArgumentException if no value is set and no default value exists
|
||||
*/
|
||||
fun <T : Serializable> getConfigValue(configProperty: ConfigProperty<T>): T {
|
||||
val appConfig = appConfigRepository.findById(configProperty.key).orElse(null)
|
||||
return if (appConfig != null) {
|
||||
getValue(appConfig.value, configProperty)
|
||||
} else {
|
||||
configProperty.default ?: throw IllegalArgumentException("No value found for key: ${configProperty.key}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value of a config property in a *not* type-safe way.
|
||||
* Used for the external API.
|
||||
*
|
||||
* @param key: The key of the config property
|
||||
* @return The current value if set or the default value
|
||||
* @throws IllegalArgumentException if no value is set and no default value exists
|
||||
*/
|
||||
fun getConfigValue(key: String): String {
|
||||
val configProperty = findConfigProperty(key)
|
||||
val appConfig = appConfigRepository.findById(configProperty.key).orElse(null)
|
||||
|
||||
return if (appConfig != null) {
|
||||
getValue(appConfig.value, configProperty).toString()
|
||||
} else {
|
||||
configProperty.default?.toString()
|
||||
?: throw IllegalArgumentException("No value found for key: ${configProperty.key}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for a specified key.
|
||||
* Checks if the value can be cast to the type defined for the config property.
|
||||
*
|
||||
* @param key: Key of the target config property
|
||||
* @param value: Value to set the config property to
|
||||
* @throws IllegalArgumentException if the value can't be cast to the type defined for the config property
|
||||
*/
|
||||
fun <T : Serializable> setConfigValue(key: String, value: T) {
|
||||
val configKey = findConfigProperty(key)
|
||||
|
||||
if (configKey.type.safeCast(value) == null) {
|
||||
throw IllegalArgumentException("Type mismatch for key: ${configKey.key}")
|
||||
}
|
||||
|
||||
val appConfig = ConfigEntry(configKey.key, value.toString())
|
||||
appConfigRepository.save(appConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a given config property to its default value if it has a default value.
|
||||
* Otherwise, delete the config key from the database.
|
||||
*
|
||||
* @param key: Key of the config property
|
||||
*/
|
||||
fun resetConfigValue(key: String) {
|
||||
val configKey = findConfigProperty(key)
|
||||
|
||||
if (configKey.default == null) {
|
||||
deleteConfig(key)
|
||||
return
|
||||
}
|
||||
|
||||
val appConfig = appConfigRepository.findById(configKey.key).orElse(null)
|
||||
if (appConfig != null) {
|
||||
appConfig.value = configKey.default.toString()
|
||||
appConfigRepository.save(appConfig)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a config property from the database
|
||||
*
|
||||
* @param key: Key of the config property
|
||||
*/
|
||||
fun deleteConfig(key: String) {
|
||||
val configKey = findConfigProperty(key)
|
||||
appConfigRepository.deleteById(configKey.key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the config property in a type-safe way.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T : Serializable> getValue(value: String, configProperty: ConfigProperty<T>): T {
|
||||
return when (configProperty.type) {
|
||||
String::class -> value as T
|
||||
Boolean::class -> value.toBoolean() as T
|
||||
Number::class -> value.toInt() as T
|
||||
Float::class -> value.toFloat() as T
|
||||
else -> {
|
||||
throw RuntimeException("Unknown config type ${configProperty.type}: '$value' for key ${configProperty.key}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a config property
|
||||
*/
|
||||
private fun findConfigProperty(key: String): ConfigProperty<*> {
|
||||
// Use reflection to get all objects defined within ConfigKey
|
||||
val configProperties = ConfigProperty::class.sealedSubclasses.flatMap { subclass ->
|
||||
subclass.objectInstance?.let { listOf(it) } ?: listOf()
|
||||
}
|
||||
|
||||
// Find the matching config key based on the string key
|
||||
return configProperties.find { it.key == key }
|
||||
?: throw IllegalArgumentException("Unknown configuration key: $key")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.grimsi.gameyfin.config.entities
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.validation.constraints.NotNull
|
||||
|
||||
@Entity
|
||||
@Table(name = "app_config")
|
||||
class ConfigEntry(
|
||||
@Id
|
||||
@NotNull
|
||||
@Column(unique = true)
|
||||
val key: String,
|
||||
|
||||
@NotNull
|
||||
var value: String
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.grimsi.gameyfin.config.persistence
|
||||
|
||||
import de.grimsi.gameyfin.config.entities.ConfigEntry
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface ConfigRepository : JpaRepository<ConfigEntry, String>
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.grimsi.gameyfin.libraries
|
||||
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import de.grimsi.gameyfin.libraries.entities.Library
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
|
||||
@Endpoint
|
||||
class LibraryEndpoint(
|
||||
private val libraryService: LibraryService
|
||||
) {
|
||||
@RolesAllowed(Roles.Names.SUPERADMIN, Roles.Names.ADMIN)
|
||||
fun getAllLibraries(): Collection<Library> {
|
||||
return libraryService.getAllLibraries()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.grimsi.gameyfin.libraries
|
||||
|
||||
import de.grimsi.gameyfin.libraries.entities.Library
|
||||
import de.grimsi.gameyfin.libraries.persistence.LibraryRepository
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class LibraryService(
|
||||
private val libraryRepository: LibraryRepository
|
||||
) {
|
||||
fun createLibrary(library: Library): Library {
|
||||
return libraryRepository.save(library)
|
||||
}
|
||||
|
||||
fun getAllLibraries(): Collection<Library> {
|
||||
return libraryRepository.findAll()
|
||||
}
|
||||
|
||||
fun deleteLibrary(library: Library) {
|
||||
libraryRepository.delete(library)
|
||||
}
|
||||
|
||||
fun updateLibrary(library: Library) {
|
||||
libraryRepository.save(library)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.grimsi.gameyfin.libraries.entities
|
||||
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
|
||||
@Entity
|
||||
class Library(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
var id: Long? = null,
|
||||
|
||||
var path: String
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.grimsi.gameyfin.libraries.persistence
|
||||
|
||||
import de.grimsi.gameyfin.libraries.entities.Library
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface LibraryRepository : JpaRepository<Library, Long>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
package de.grimsi.gameyfin.meta
|
||||
|
||||
enum class Roles(val roleName: String) {
|
||||
SUPERADMIN(Names.SUPERADMIN),
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
package de.grimsi.gameyfin.meta
|
||||
|
||||
import com.vaadin.flow.spring.security.VaadinWebSecurity
|
||||
import org.springframework.context.annotation.Bean
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.grimsi.gameyfin.meta
|
||||
|
||||
import de.grimsi.gameyfin.meta.annotations.DynamicAccessInterceptor
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
@Configuration
|
||||
class WebConfig(val dynamicAccessInterceptor: DynamicAccessInterceptor) : WebMvcConfigurer {
|
||||
|
||||
override fun addInterceptors(registry: InterceptorRegistry) {
|
||||
registry.addInterceptor(dynamicAccessInterceptor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.grimsi.gameyfin.meta.annotations
|
||||
|
||||
import de.grimsi.gameyfin.config.ConfigProperty
|
||||
import de.grimsi.gameyfin.config.ConfigService
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.method.HandlerMethod
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
|
||||
@Component
|
||||
class DynamicAccessInterceptor(
|
||||
private val configService: ConfigService
|
||||
) : HandlerInterceptor {
|
||||
|
||||
override fun preHandle(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
handler: Any
|
||||
): Boolean {
|
||||
val handlerMethod = (handler as? HandlerMethod) ?: return true
|
||||
val method = handlerMethod.method
|
||||
|
||||
// Check if method is annotated with @DynamicPublicAccess
|
||||
if (method.isAnnotationPresent(DynamicPublicAccess::class.java)) {
|
||||
// Check if user is authenticated or public access is enabled
|
||||
if (request.userPrincipal != null || configService.getConfigValue(ConfigProperty.LibraryAllowPublicAccess)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Deny access if user is not logged in and public access is disabled
|
||||
response.status = HttpServletResponse.SC_UNAUTHORIZED
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.grimsi.gameyfin.meta.annotations
|
||||
|
||||
import kotlin.annotation.AnnotationRetention.RUNTIME
|
||||
import kotlin.annotation.AnnotationTarget.FUNCTION
|
||||
|
||||
/**
|
||||
* This annotation is used on endpoint methods which can be switched between publicly accessible and
|
||||
* only accessible for registered users.
|
||||
* One example would be the main library view.
|
||||
*/
|
||||
|
||||
@Target(FUNCTION)
|
||||
@Retention(RUNTIME)
|
||||
annotation class DynamicPublicAccess
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.grimsi.gameyfin.setup
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import de.grimsi.gameyfin.users.UserService
|
||||
import de.grimsi.gameyfin.users.entities.Role
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package de.grimsi.gameyfin.setup
|
||||
|
||||
import com.vaadin.flow.server.auth.AnonymousAllowed
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import de.grimsi.gameyfin.users.RoleService
|
||||
import de.grimsi.gameyfin.users.UserService
|
||||
import de.grimsi.gameyfin.users.dto.UserInfo
|
||||
import de.grimsi.gameyfin.users.dto.UserRegistration
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import com.vaadin.hilla.exception.EndpointException
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import de.grimsi.gameyfin.users.RoleService
|
||||
import de.grimsi.gameyfin.users.UserService
|
||||
import de.grimsi.gameyfin.users.dto.UserInfoDto
|
||||
import de.grimsi.gameyfin.users.dto.UserRegistrationDto
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
|
||||
@Endpoint
|
||||
class SetupEndpoint(
|
||||
@@ -22,7 +22,7 @@ class SetupEndpoint(
|
||||
}
|
||||
|
||||
@AnonymousAllowed
|
||||
fun registerSuperAdmin(superAdminRegistration: UserRegistration): UserInfo {
|
||||
fun registerSuperAdmin(superAdminRegistration: UserRegistrationDto): UserInfoDto {
|
||||
if (setupService.isSetupCompleted()) throw EndpointException("Setup already completed")
|
||||
|
||||
val user = User(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.grimsi.gameyfin.setup
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import de.grimsi.gameyfin.users.RoleService
|
||||
import de.grimsi.gameyfin.users.UserService
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.grimsi.gameyfin.system
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
|
||||
@Endpoint
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.grimsi.gameyfin.users
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import de.grimsi.gameyfin.users.entities.Role
|
||||
import de.grimsi.gameyfin.users.persistence.RoleRepository
|
||||
import jakarta.transaction.Transactional
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package de.grimsi.gameyfin.users
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import de.grimsi.gameyfin.users.dto.UserInfo
|
||||
import de.grimsi.gameyfin.users.dto.UserRegistration
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import de.grimsi.gameyfin.users.dto.UserInfoDto
|
||||
import de.grimsi.gameyfin.users.dto.UserRegistrationDto
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
import jakarta.annotation.security.PermitAll
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
@@ -16,19 +16,19 @@ class UserEndpoint(
|
||||
) {
|
||||
|
||||
@PermitAll
|
||||
fun getUserInfo(): UserInfo {
|
||||
fun getUserInfo(): UserInfoDto {
|
||||
val auth: Authentication = SecurityContextHolder.getContext().authentication
|
||||
val authorities: List<String> = auth.authorities.map { g: GrantedAuthority -> g.authority }
|
||||
return UserInfo(username = auth.name, roles = authorities)
|
||||
return UserInfoDto(username = auth.name, roles = authorities)
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
fun registerUser(registration: UserRegistration): UserInfo {
|
||||
fun registerUser(registration: UserRegistrationDto): UserInfoDto {
|
||||
val user: User = registerUser(registration, listOf(Roles.USER))
|
||||
return userService.toUserInfo(user)
|
||||
}
|
||||
|
||||
private fun registerUser(registration: UserRegistration, roles: List<Roles>): User {
|
||||
private fun registerUser(registration: UserRegistrationDto, roles: List<Roles>): User {
|
||||
val user = User(
|
||||
username = registration.username,
|
||||
password = registration.password,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.grimsi.gameyfin.users
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import de.grimsi.gameyfin.users.dto.UserInfo
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import de.grimsi.gameyfin.users.dto.UserInfoDto
|
||||
import de.grimsi.gameyfin.users.entities.Role
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
import de.grimsi.gameyfin.users.persistence.UserRepository
|
||||
@@ -50,8 +50,8 @@ class UserService(
|
||||
return userRepository.save(user)
|
||||
}
|
||||
|
||||
fun toUserInfo(user: User): UserInfo {
|
||||
return UserInfo(
|
||||
fun toUserInfo(user: User): UserInfoDto {
|
||||
return UserInfoDto(
|
||||
username = user.username,
|
||||
email = user.email,
|
||||
roles = user.roles.map { r -> r.rolename }
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package de.grimsi.gameyfin.users.dto
|
||||
|
||||
data class UserInfo(
|
||||
data class UserInfoDto(
|
||||
val username: String,
|
||||
val email: String? = null,
|
||||
val roles: List<String>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package de.grimsi.gameyfin.users.dto
|
||||
|
||||
data class UserRegistration(
|
||||
data class UserRegistrationDto(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val email: String
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package de.grimsi.gameyfin.users.util
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
|
||||
fun UserDetails.hasRole(role: Roles): Boolean {
|
||||
|
||||
Reference in New Issue
Block a user