Implement user management

Implement dark mode for UI
This commit is contained in:
grimsi
2024-02-06 01:56:38 +01:00
parent 310bff3b8c
commit 64280579f2
28 changed files with 445 additions and 325 deletions
-56
View File
@@ -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
-92
View File
@@ -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
-114
View File
@@ -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
View File
@@ -45,4 +45,4 @@ out/
/backend/src/main/resources/static/
/docker/docker-compose.yml
/.gameyfin/
/frontend/
/frontend/generated
-10
View File
@@ -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
View File
@@ -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
+23
View File
@@ -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>
+12
View File
@@ -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")
}
@@ -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,4 +1,4 @@
package de.grimsi.gameyfin.security
package de.grimsi.gameyfin.users.util
import org.springframework.security.core.userdetails.UserDetails
+7 -1
View File
@@ -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