mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-18 08:15:49 +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/
|
/backend/src/main/resources/static/
|
||||||
/docker/docker-compose.yml
|
/docker/docker-compose.yml
|
||||||
/.gameyfin/
|
/.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"
|
kotlin("plugin.jpa") version "1.9.22"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allOpen {
|
||||||
|
annotations("javax.persistence.Entity", "javax.persistence.MappedSuperclass", "javax.persistence.Embedabble")
|
||||||
|
}
|
||||||
|
|
||||||
group = "de.grimsi"
|
group = "de.grimsi"
|
||||||
version = "2.0.0-SNAPSHOT"
|
version = "2.0.0-SNAPSHOT"
|
||||||
|
|
||||||
@@ -33,18 +37,26 @@ repositories {
|
|||||||
extra["vaadinVersion"] = "24.3.3"
|
extra["vaadinVersion"] = "24.3.3"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Sprint Boot
|
||||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
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")
|
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
|
// Frontend
|
||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
|
||||||
implementation("com.vaadin:vaadin-spring-boot-starter")
|
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.karibudsl:karibu-dsl-v23:2.1.0")
|
||||||
implementation("com.github.mvysny.karibu-tools:karibu-tools-23:0.19")
|
implementation("com.github.mvysny.karibu-tools:karibu-tools-23:0.19")
|
||||||
implementation("com.flowingcode.addons:font-awesome-iron-iconset:5.2.2")
|
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")
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
|
|
||||||
// Development
|
// 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 com.vaadin.flow.spring.security.VaadinWebSecurity
|
||||||
import de.grimsi.gameyfin.ui.views.LoginView
|
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.builders.WebSecurity
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer
|
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer
|
||||||
import org.springframework.security.core.userdetails.User
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
import org.springframework.security.core.userdetails.UserDetails
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
|
||||||
import org.springframework.security.provisioning.UserDetailsManager
|
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
|
||||||
|
|
||||||
|
|
||||||
@@ -39,21 +37,8 @@ class SecurityConfiguration : VaadinWebSecurity() {
|
|||||||
super.configure(web)
|
super.configure(web)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Just for testing
|
|
||||||
*/
|
|
||||||
@Bean
|
@Bean
|
||||||
fun userDetailsService(): UserDetailsManager {
|
fun passwordEncoder(): PasswordEncoder {
|
||||||
val user: UserDetails =
|
return BCryptPasswordEncoder()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.karibudsl.v10.*
|
||||||
import com.github.mvysny.kaributools.tooltip
|
import com.github.mvysny.kaributools.tooltip
|
||||||
import com.vaadin.flow.component.ClickEvent
|
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.menubar.MenuBarVariant
|
||||||
|
import com.vaadin.flow.component.notification.Notification
|
||||||
import com.vaadin.flow.component.orderedlayout.FlexComponent
|
import com.vaadin.flow.component.orderedlayout.FlexComponent
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout
|
||||||
import com.vaadin.flow.router.RouterLayout
|
import com.vaadin.flow.router.RouterLayout
|
||||||
import com.vaadin.flow.spring.security.AuthenticationContext
|
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.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
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
|
||||||
|
@JsModule("./scripts/prefers-color-scheme.js")
|
||||||
class MainLayout(@field:Transient private val authContext: AuthenticationContext) : KComposite(), RouterLayout {
|
class MainLayout(
|
||||||
|
@field:Transient private val authContext: AuthenticationContext,
|
||||||
|
@Autowired private val setupService: SetupService,
|
||||||
private val appLayout: AppLayout
|
@Autowired private val themeService: ThemeService
|
||||||
|
) : KComposite(), RouterLayout {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
if (!setupService.isSetupCompleted()) UI.getCurrent().navigate(SetupView::class.java)
|
||||||
|
|
||||||
val user = authContext.getAuthenticatedUser(UserDetails::class.java).get()
|
val user = authContext.getAuthenticatedUser(UserDetails::class.java).get()
|
||||||
|
|
||||||
appLayout = ui {
|
ui {
|
||||||
|
|
||||||
appLayout {
|
appLayout {
|
||||||
navbar {
|
navbar {
|
||||||
@@ -46,12 +55,30 @@ class MainLayout(@field:Transient private val authContext: AuthenticationContext
|
|||||||
colorIndex = user.username[0].code.toByte().mod(6)
|
colorIndex = user.username[0].code.toByte().mod(6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val toggleDarkModeIcon =
|
||||||
|
FontAwesome.Solid.CIRCLE_HALF_STROKE.create { _ -> themeService.toggleTheme() }
|
||||||
|
iconButton(toggleDarkModeIcon)
|
||||||
|
|
||||||
menuBar {
|
menuBar {
|
||||||
addThemeVariants(MenuBarVariant.LUMO_ICON)
|
addThemeVariants(MenuBarVariant.LUMO_ICON)
|
||||||
item(a) {
|
item(a) {
|
||||||
item(menuItem(FontAwesome.Solid.USER, "Profile"))
|
item(
|
||||||
if (user.isAdmin()) item(menuItem(FontAwesome.Solid.COG, "Administration"))
|
menuItem(
|
||||||
item(menuItem(FontAwesome.Solid.QUESTION_CIRCLE, "Help"))
|
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() })
|
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.login.LoginForm
|
||||||
import com.vaadin.flow.component.orderedlayout.FlexComponent
|
import com.vaadin.flow.component.orderedlayout.FlexComponent
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout
|
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.PageTitle
|
||||||
import com.vaadin.flow.router.Route
|
import com.vaadin.flow.router.Route
|
||||||
import com.vaadin.flow.server.auth.AnonymousAllowed
|
import com.vaadin.flow.server.auth.AnonymousAllowed
|
||||||
@@ -15,7 +13,7 @@ import de.grimsi.gameyfin.ui.resources.PublicResources
|
|||||||
@Route("login")
|
@Route("login")
|
||||||
@PageTitle("Login")
|
@PageTitle("Login")
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
class LoginView : VerticalLayout(), BeforeEnterObserver {
|
class LoginView : VerticalLayout() {
|
||||||
|
|
||||||
private var login: LoginForm
|
private var login: LoginForm
|
||||||
|
|
||||||
@@ -35,17 +33,4 @@ class LoginView : VerticalLayout(), BeforeEnterObserver {
|
|||||||
action = "login"
|
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
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
|
||||||
@@ -3,3 +3,9 @@ vaadin.whitelisted-packages:
|
|||||||
- org.vaadin
|
- org.vaadin
|
||||||
- dev.hilla
|
- dev.hilla
|
||||||
- com.flowingcode
|
- com.flowingcode
|
||||||
|
|
||||||
|
spring:
|
||||||
|
jpa:
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
globally_quoted_identifiers: true
|
||||||
Reference in New Issue
Block a user