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