mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Implement user management
Implement dark mode for UI
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+1
-1
@@ -45,4 +45,4 @@ out/
|
||||
/backend/src/main/resources/static/
|
||||
/docker/docker-compose.yml
|
||||
/.gameyfin/
|
||||
/frontend/
|
||||
/frontend/generated
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
<servers>
|
||||
<server>
|
||||
<id>github</id>
|
||||
<username>${env.GITHUB_ACTOR}</username>
|
||||
<password>${env.GITHUB_TOKEN}</password>
|
||||
</server>
|
||||
</servers>
|
||||
</settings>
|
||||
+15
-3
@@ -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
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This file is auto-generated by Vaadin.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body, #outlet {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- This outlet div is where the views are rendered -->
|
||||
<div id="outlet"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
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