diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 1cea2f5..0000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,56 +0,0 @@
-name: Gameyfin CI Pipeline
-
-on:
- push:
- branches:
- - main
- pull_request:
- types: [opened, synchronize, reopened]
- workflow_dispatch:
-
-jobs:
- build:
- name: Build, Test & Scan
- runs-on: ubuntu-latest
- if: "!contains(github.event.head_commit.message, '[ci skip]')"
- steps:
- - name: Git checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
-
- - name: Set up JDK
- uses: actions/setup-java@v4
- with:
- java-version: '21'
- distribution: 'temurin'
-
- - name: Cache SonarCloud packages
- uses: actions/cache@v4
- with:
- path: ~/.sonar/cache
- key: ${{ runner.os }}-sonar
- restore-keys: ${{ runner.os }}-sonar
-
- - name: Cache Maven packages
- uses: actions/cache@v4
- with:
- path: ~/.m2
- key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- restore-keys: ${{ runner.os }}-m2
-
- - name: Extract Maven project version
- id: project
- run: echo "GAMEYFIN_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_OUTPUT
-
- - name: Build and analyze
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=grimsi_gameyfin
-
- - name: Upload build artifact
- uses: actions/upload-artifact@v4
- with:
- name: gameyfin-${{ steps.project.outputs.GAMEYFIN_VERSION }}.jar
- path: backend/target/gameyfin-*.jar
diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml
deleted file mode 100644
index c745f41..0000000
--- a/.github/workflows/docker-build-push.yml
+++ /dev/null
@@ -1,92 +0,0 @@
-name: Gameyfin Docker Build & Push
-
-on:
- workflow_dispatch:
- inputs:
- branch:
- description: "The branch to checkout when cutting the release."
- required: true
- default: "main"
- tag:
- description: "Docker image tag."
- required: true
- default: "X.Y.Z"
-
-jobs:
- release:
- runs-on: ubuntu-latest
- name: Release
- steps:
- - name: Git checkout
- uses: actions/checkout@v4
- with:
- ref: ${{ github.event.inputs.branch }}
-
- - name: Set up JDK
- uses: actions/setup-java@v4
- with:
- java-version: '21'
- distribution: 'temurin'
- cache: 'maven'
-
- - name: Configure Git User
- run: |
- git config user.email "actions@github.com"
- git config user.name "GitHub Actions"
-
- - name: Maven Package
- run: mvn package -B -s .maven_settings.xml -DreleaseVersion=${{ github.event.inputs.tag }} -Darguments="-Dmaven.deploy.skip=true -Dmaven.test.skip=true -Dmaven.javadoc.skip=true"
- env:
- GITHUB_ACTOR: ${{ github.actor }}
- GITHUB_TOKEN: ${{ github.token }}
-
- - name: Docker meta
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: |
- grimsi/gameyfin
- tags: |
- type=semver,pattern={{version}},value=${{ github.event.inputs.tag }}
- type=semver,pattern={{major}}.{{minor}},value=${{ github.event.inputs.tag }}
- type=semver,pattern={{major}},value=${{ github.event.inputs.tag }}
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Cache Docker layers
- uses: actions/cache@v4
- with:
- path: /tmp/.buildx-cache
- key: ${{ runner.os }}-buildx-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-buildx-
-
- - name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- - name: Build and push
- uses: docker/build-push-action@v5
- with:
- context: .
- file: ./docker/Dockerfile
- platforms: linux/amd64,linux/arm64
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=local,src=/tmp/.buildx-cache
- cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
-
- - # Temp fix
- # https://github.com/docker/build-push-action/issues/252
- # https://github.com/moby/buildkit/issues/1896
- name: Move Docker cache (temp fix)
- run: |
- rm -rf /tmp/.buildx-cache
- mv /tmp/.buildx-cache-new /tmp/.buildx-cache
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index eedd0ce..0000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,114 +0,0 @@
-name: Gameyfin Release
-
-on:
- workflow_dispatch:
- inputs:
- branch:
- description: "The branch to checkout when cutting the release."
- required: true
- default: "main"
- releaseVersion:
- description: "Default version to use when preparing a release."
- required: true
- default: "X.Y.Z"
- developmentVersion:
- description: "Default version to use for new local working copy."
- required: true
- default: "X.Y.Z-SNAPSHOT"
-
-jobs:
- release:
- runs-on: ubuntu-latest
- name: Release
- steps:
- - name: Git checkout
- uses: actions/checkout@v4
- with:
- ref: ${{ github.event.inputs.branch }}
-
- - name: Set up JDK
- uses: actions/setup-java@v4
- with:
- java-version: '21'
- distribution: 'temurin'
- cache: 'maven'
-
- - name: Configure Git User
- run: |
- git config user.email "actions@github.com"
- git config user.name "GitHub Actions"
-
- - name: Maven Release
- run: mvn release:prepare release:perform -B -s .maven_settings.xml -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -DdevelopmentVersion=${{ github.event.inputs.developmentVersion }} -Darguments="-Dmaven.deploy.skip=true -Dmaven.test.skip=true -Dmaven.javadoc.skip=true"
- env:
- GITHUB_ACTOR: ${{ github.actor }}
- GITHUB_TOKEN: ${{ github.token }}
-
- - name: Git tag
- uses: mathieudutour/github-tag-action@v6.1
- with:
- github_token: ${{ github.token }}
- default_bump: false
- custom_tag: ${{ github.event.inputs.releaseVersion }}
-
- - name: Github Release
- uses: "marvinpinto/action-automatic-releases@v1.2.1"
- with:
- repo_token: ${{ github.token }}
- prerelease: false
- automatic_release_tag: v${{ github.event.inputs.releaseVersion }}
- files: |
- LICENSE.md
- backend/target/gameyfin-*.jar
- config/gameyfin.properties
-
- - name: Docker meta
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: |
- grimsi/gameyfin
- tags: |
- type=semver,pattern={{version}},value=${{ github.event.inputs.releaseVersion }}
- type=semver,pattern={{major}}.{{minor}},value=${{ github.event.inputs.releaseVersion }}
- type=semver,pattern={{major}},value=${{ github.event.inputs.releaseVersion }}
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Cache Docker layers
- uses: actions/cache@v4
- with:
- path: /tmp/.buildx-cache
- key: ${{ runner.os }}-buildx-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-buildx-
-
- - name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- - name: Build and push
- uses: docker/build-push-action@v5
- with:
- context: .
- file: ./docker/Dockerfile
- platforms: linux/amd64,linux/arm64
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=local,src=/tmp/.buildx-cache
- cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
-
- - # Temp fix
- # https://github.com/docker/build-push-action/issues/252
- # https://github.com/moby/buildkit/issues/1896
- name: Move Docker cache (temp fix)
- run: |
- rm -rf /tmp/.buildx-cache
- mv /tmp/.buildx-cache-new /tmp/.buildx-cache
diff --git a/.gitignore b/.gitignore
index b199ffd..4091a91 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,4 @@ out/
/backend/src/main/resources/static/
/docker/docker-compose.yml
/.gameyfin/
-/frontend/
+/frontend/generated
diff --git a/.maven_settings.xml b/.maven_settings.xml
deleted file mode 100644
index 2d0b3b8..0000000
--- a/.maven_settings.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- github
- ${env.GITHUB_ACTOR}
- ${env.GITHUB_TOKEN}
-
-
-
diff --git a/build.gradle.kts b/build.gradle.kts
index 480b1d3..28f0fca 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -9,6 +9,10 @@ plugins {
kotlin("plugin.jpa") version "1.9.22"
}
+allOpen {
+ annotations("javax.persistence.Entity", "javax.persistence.MappedSuperclass", "javax.persistence.Embedabble")
+}
+
group = "de.grimsi"
version = "2.0.0-SNAPSHOT"
@@ -33,18 +37,26 @@ repositories {
extra["vaadinVersion"] = "24.3.3"
dependencies {
+ // Sprint Boot
implementation("org.springframework.boot:spring-boot-starter-actuator")
- implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
+ // Logging
+ implementation("io.github.oshai:kotlin-logging-jvm:6.0.3")
+
+ // Persistence
+ implementation("org.springframework.boot:spring-boot-starter-data-jpa")
+ implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.7")
+
// Frontend
- implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("com.vaadin:vaadin-spring-boot-starter")
+ implementation("org.springframework.boot:spring-boot-starter-security")
+ 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")
// Development
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..d36e593
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/scripts/prefers-color-scheme.js b/frontend/scripts/prefers-color-scheme.js
new file mode 100644
index 0000000..61db7da
--- /dev/null
+++ b/frontend/scripts/prefers-color-scheme.js
@@ -0,0 +1,12 @@
+window.applyTheme = () => {
+ const theme = window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "";
+ document.documentElement.setAttribute("theme", theme);
+};
+window
+ .matchMedia("(prefers-color-scheme: dark)")
+ .addEventListener('change', function () {
+ window.applyTheme()
+ });
+window.applyTheme();
\ No newline at end of file
diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle
index 8e8d4b3..4edcbd3 100644
Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ
diff --git a/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt b/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt
new file mode 100644
index 0000000..09d5891
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt
@@ -0,0 +1,6 @@
+package de.grimsi.gameyfin.config
+
+enum class Roles(val roleName: String) {
+ ADMIN("ROLE_ADMIN"),
+ USER("ROLE_USER")
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/security/SecurityConfiguration.kt b/src/main/kotlin/de/grimsi/gameyfin/config/SecurityConfiguration.kt
similarity index 66%
rename from src/main/kotlin/de/grimsi/gameyfin/security/SecurityConfiguration.kt
rename to src/main/kotlin/de/grimsi/gameyfin/config/SecurityConfiguration.kt
index 29e8a22..2d09198 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/security/SecurityConfiguration.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/config/SecurityConfiguration.kt
@@ -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()
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt
new file mode 100644
index 0000000..d45b57a
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupFilter.kt b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupFilter.kt
new file mode 100644
index 0000000..0d02252
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupFilter.kt
@@ -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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt
new file mode 100644
index 0000000..e8d9615
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt
@@ -0,0 +1,11 @@
+package de.grimsi.gameyfin.setup
+
+import org.springframework.stereotype.Service
+
+@Service
+class SetupService {
+
+ fun isSetupCompleted() : Boolean {
+ return false
+ }
+}
\ 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
index b60a215..8cb2667 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/MainLayout.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/MainLayout.kt
@@ -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() })
}
}
diff --git a/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/SetupLayout.kt b/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/SetupLayout.kt
new file mode 100644
index 0000000..7a06256
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/ui/layouts/SetupLayout.kt
@@ -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)
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000..2118ae4
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/ui/services/ThemeService.kt
@@ -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)
+ }
+}
\ 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
index a82b925..ed45101 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/ui/views/LoginView.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/ui/views/LoginView.kt
@@ -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
- }
- }
- }
-
}
\ 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
new file mode 100644
index 0000000..97ed192
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/ui/views/SetupView.kt
@@ -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")
+ }
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000..0e2ab65
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt
@@ -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): List {
+ return roles.map { r -> SimpleGrantedAuthority(r.rolename) }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/entities/Avatar.kt b/src/main/kotlin/de/grimsi/gameyfin/users/entities/Avatar.kt
new file mode 100644
index 0000000..283396e
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/Avatar.kt
@@ -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
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt b/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt
new file mode 100644
index 0000000..83cd716
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt
@@ -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 = emptyList()
+)
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt b/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt
new file mode 100644
index 0000000..3947268
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt
@@ -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 = emptyList()
+)
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/persistence/AvatarContentStore.kt b/src/main/kotlin/de/grimsi/gameyfin/users/persistence/AvatarContentStore.kt
new file mode 100644
index 0000000..0d45092
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/persistence/AvatarContentStore.kt
@@ -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
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/persistence/RoleRepository.kt b/src/main/kotlin/de/grimsi/gameyfin/users/persistence/RoleRepository.kt
new file mode 100644
index 0000000..5752dde
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/persistence/RoleRepository.kt
@@ -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 {
+ fun findByRolename(roleName: String): Role?
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/persistence/UserRepository.kt b/src/main/kotlin/de/grimsi/gameyfin/users/persistence/UserRepository.kt
new file mode 100644
index 0000000..5aca5e3
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/persistence/UserRepository.kt
@@ -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 {
+ fun findByUsername(userName: String): User?
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/security/UserDetailsExtensions.kt b/src/main/kotlin/de/grimsi/gameyfin/users/util/UserDetailsExtensions.kt
similarity index 87%
rename from src/main/kotlin/de/grimsi/gameyfin/security/UserDetailsExtensions.kt
rename to src/main/kotlin/de/grimsi/gameyfin/users/util/UserDetailsExtensions.kt
index a4758fd..c5cefa0 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/security/UserDetailsExtensions.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/util/UserDetailsExtensions.kt
@@ -1,4 +1,4 @@
-package de.grimsi.gameyfin.security
+package de.grimsi.gameyfin.users.util
import org.springframework.security.core.userdetails.UserDetails
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 84cceed..ab598fa 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -2,4 +2,10 @@ vaadin.whitelisted-packages:
- com.vaadin
- org.vaadin
- dev.hilla
- - com.flowingcode
\ No newline at end of file
+ - com.flowingcode
+
+spring:
+ jpa:
+ properties:
+ hibernate:
+ globally_quoted_identifiers: true
\ No newline at end of file