Implement frontend structure

This commit is contained in:
grimsi
2024-02-04 19:15:34 +01:00
parent fc84f92e23
commit 3c8d78d22b
16 changed files with 386 additions and 37 deletions
+2 -1
View File
@@ -44,4 +44,5 @@ out/
/data/
/backend/src/main/resources/static/
/docker/docker-compose.yml
/.gameyfin/
/.gameyfin/
/frontend/
+45 -34
View File
@@ -1,63 +1,74 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.2.2"
id("io.spring.dependency-management") version "1.1.4"
id("com.vaadin") version "24.3.3"
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22"
kotlin("plugin.jpa") version "1.9.22"
id("org.springframework.boot") version "3.2.2"
id("io.spring.dependency-management") version "1.1.4"
id("com.vaadin") version "24.3.3"
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22"
kotlin("plugin.jpa") version "1.9.22"
}
group = "de.grimsi"
version = "0.0.1-SNAPSHOT"
version = "2.0.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_21
sourceCompatibility = JavaVersion.VERSION_21
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
mavenCentral()
maven {
name = "Vaadin Addons"
url = uri("https://maven.vaadin.com/vaadin-addons")
}
}
extra["vaadinVersion"] = "24.3.3"
dependencies {
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-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.jetbrains.kotlin:kotlin-reflect")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
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")
// 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("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("org.jetbrains.kotlin:kotlin-reflect")
// Development
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
}
dependencyManagement {
imports {
mavenBom("com.vaadin:vaadin-bom:${property("vaadinVersion")}")
}
imports {
mavenBom("com.vaadin:vaadin-bom:${property("vaadinVersion")}")
}
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "21"
}
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "21"
}
}
tasks.withType<Test> {
useJUnitPlatform()
useJUnitPlatform()
}
+31
View File
@@ -0,0 +1,31 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
projectJDK: "21" #(Applied in CI/CD pipeline)
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-jvm:latest
+32
View File
@@ -0,0 +1,32 @@
This directory is automatically generated by Vaadin and contains the pre-compiled
frontend files/resources for your project (frontend development bundle).
It should be added to Version Control System and committed, so that other developers
do not have to compile it again.
Frontend development bundle is automatically updated when needed:
- an npm/pnpm package is added with @NpmPackage or directly into package.json
- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript
- Vaadin add-on with front-end customizations is added
- Custom theme imports/assets added into 'theme.json' file
- Exported web component is added.
If your project development needs a hot deployment of the frontend changes,
you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions):
- set `vaadin.frontend.hotdeploy=true` in `application.properties`
- configure `vaadin-maven-plugin`:
```
<configuration>
<frontendHotdeploy>true</frontendHotdeploy>
</configuration>
```
- configure `jetty-maven-plugin`:
```
<configuration>
<systemProperties>
<vaadin.frontend.hotdeploy>true</vaadin.frontend.hotdeploy>
</systemProperties>
</configuration>
```
Read more [about Vaadin development mode](https://vaadin.com/docs/next/configuration/development-mode/#pre-compiled-front-end-bundle-for-faster-start-up).
Binary file not shown.
@@ -3,9 +3,10 @@ package de.grimsi.gameyfin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class GameyfinApplication
fun main(args: Array<String>) {
runApplication<GameyfinApplication>(*args)
}
}
@@ -0,0 +1,59 @@
package de.grimsi.gameyfin.security
import com.vaadin.flow.spring.security.VaadinWebSecurity
import de.grimsi.gameyfin.ui.views.LoginView
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
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.web.util.matcher.AntPathRequestMatcher
@EnableWebSecurity
@Configuration
class SecurityConfiguration : VaadinWebSecurity() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
// Configure your static resources with public access before calling super.configure(HttpSecurity) as it adds final anyRequest matcher
http.authorizeHttpRequests { auth: AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry ->
auth.requestMatchers(AntPathRequestMatcher("/public/**")).permitAll()
}
// Configure your static resources with public access before calling
// super.configure(HttpSecurity) as it adds final anyRequest matcher
super.configure(http)
// This is important to register your login view to the navigation access control mechanism:
setLoginView(http, LoginView::class.java)
}
@Throws(Exception::class)
public override fun configure(web: WebSecurity) {
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)
}
}
@@ -0,0 +1,11 @@
package de.grimsi.gameyfin.security
import org.springframework.security.core.userdetails.UserDetails
fun UserDetails.hasRole(role: String): Boolean {
return this.authorities.map { a -> a.authority }.contains("ROLE_".plus(role))
}
fun UserDetails.isAdmin(): Boolean {
return hasRole("ADMIN")
}
@@ -0,0 +1,87 @@
package de.grimsi.gameyfin.ui.layouts
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.menubar.MenuBarVariant
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.ui.resources.PublicResources
import org.springframework.security.core.userdetails.UserDetails
class MainLayout(@field:Transient private val authContext: AuthenticationContext) : KComposite(), RouterLayout {
private val appLayout: AppLayout
init {
val user = authContext.getAuthenticatedUser(UserDetails::class.java).get()
appLayout = ui {
appLayout {
navbar {
flexLayout {
setWidthFull()
alignItems = FlexComponent.Alignment.CENTER
image(PublicResources.GAMEYFIN_LOGO_WHITE_BORDER.path) {
setWidthFull()
height = "40px"
className = "header-logo"
}
horizontalLayout {
alignItems = FlexComponent.Alignment.CENTER
val a = avatar(user.username) {
tooltip = user.username
abbreviation = user.username.take(2).uppercase()
colorIndex = user.username[0].code.toByte().mod(6)
}
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.SIGN_OUT, "Sign out") { _ -> authContext.logout() })
}
}
}
}
}
}
}
}
private fun menuItem(icon: FontAwesome.Solid, title: String): HorizontalLayout {
return HorizontalLayout().apply {
alignItems = FlexComponent.Alignment.CENTER
justifyContentMode = FlexComponent.JustifyContentMode.START
val faIcon = icon.create()
faIcon.setSize("var(--lumo-icon-size-s)")
add(faIcon)
text(title)
}
}
private fun menuItem(
icon: FontAwesome.Solid,
title: String,
action: (ClickEvent<HorizontalLayout>) -> Unit
): HorizontalLayout {
return menuItem(icon, title).apply {
onLeftClick(action)
}
}
}
@@ -0,0 +1,6 @@
package de.grimsi.gameyfin.ui.resources
enum class PublicResources(val path: String) {
GAMEYFIN_LOGO_WHITE("public/images/Gameyfin_Logo_White.svg"),
GAMEYFIN_LOGO_WHITE_BORDER("public/images/Gameyfin_Logo_White_BORDER.svg")
}
@@ -0,0 +1,50 @@
package de.grimsi.gameyfin.ui.views
import com.github.mvysny.karibudsl.v10.image
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
import de.grimsi.gameyfin.ui.resources.PublicResources
@Route("login")
@PageTitle("Login")
@AnonymousAllowed
class LoginView : VerticalLayout(), BeforeEnterObserver {
private var login: LoginForm
init {
setSizeFull()
justifyContentMode = FlexComponent.JustifyContentMode.CENTER
alignItems = FlexComponent.Alignment.CENTER
image {
height = "100px"
src = PublicResources.GAMEYFIN_LOGO_WHITE_BORDER.path
setAlt("Gameyfin")
}
login = loginForm {
addClassName("login-view")
action = "login"
}
}
override fun beforeEnter(event: BeforeEnterEvent?) {
if (event != null) {
if (event.location
.queryParameters
.parameters
.containsKey("error")) {
login.isError = true
}
}
}
}
@@ -0,0 +1,22 @@
package de.grimsi.gameyfin.ui.views
import com.github.mvysny.karibudsl.v10.h1
import com.github.mvysny.karibudsl.v10.pre
import com.github.mvysny.karibudsl.v10.verticalLayout
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.Route
import de.grimsi.gameyfin.ui.layouts.MainLayout
import jakarta.annotation.security.PermitAll
@Route("", layout = MainLayout::class)
@PermitAll
class MainView : VerticalLayout() {
init {
verticalLayout {
h1 { text = "Gameyfin main page" }
pre { text = "Work in progress" }
}
}
}
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 450 414">
<defs>
<style>.cls-1{fill:#fff;}</style>
</defs>
<title>Element 11</title>
<g>
<g>
<polygon class="cls-1" points="234 60.48 234 85.23 256 54.32 234 60.48"/>
<polygon class="cls-1" points="450 0 324 35.28 253.125 118.125 450 63 450 0"/>
<polygon class="cls-1" points="234 348.48 306 328.32 306 184.32 450 144 450 90 234 150.48 234 348.48"/>
<polygon class="cls-1" points="72 177.84 192 144.24 216 110.52 216 65.52 0 126 0 414 72 312.84 72 177.84"/>
<polygon class="cls-1"
points="144 245.68 144 301.68 81 319.32 0 414 216 353.52 216 209.52 162 224.64 144 245.68"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 768 B

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 451.94942 416.224">
<defs>
<style>.cls-1{fill:#fff;stroke:#000;stroke-miterlimit:10;}</style>
</defs>
<title>Element 10</title>
<g>
<g>
<polygon class="cls-1" points="235.449 61.139 235.449 85.889 257.449 54.979 235.449 61.139"/>
<polygon class="cls-1" points="451.449 0.659 325.449 35.939 254.574 118.784 451.449 63.659 451.449 0.659"/>
<polygon class="cls-1"
points="235.449 349.139 307.449 328.979 307.449 184.979 451.449 144.659 451.449 90.659 235.449 151.139 235.449 349.139"/>
<polygon class="cls-1"
points="73.449 178.499 193.449 144.899 217.449 111.179 217.449 66.179 1.449 126.659 1.449 414.659 73.449 313.499 73.449 178.499"/>
<polygon class="cls-1"
points="145.449 246.339 145.449 302.339 82.449 319.979 1.449 414.659 217.449 354.179 217.449 210.179 163.449 225.299 145.449 246.339"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1 +0,0 @@
+5
View File
@@ -0,0 +1,5 @@
vaadin.whitelisted-packages:
- com.vaadin
- org.vaadin
- dev.hilla
- com.flowingcode