diff --git a/.run/Generate Hilla sources.run.xml b/.run/Generate Hilla sources.run.xml new file mode 100644 index 0000000..7f6f729 --- /dev/null +++ b/.run/Generate Hilla sources.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e356a7e..bbbbe4a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,13 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - id("org.springframework.boot") version "3.2.2" - id("io.spring.dependency-management") version "1.1.4" + val kotlinVersion = "2.0.0" + id("org.springframework.boot") version "3.3.0" + id("io.spring.dependency-management") version "1.1.5" id("dev.hilla") version "2.5.6" - kotlin("jvm") version "1.9.22" - kotlin("plugin.spring") version "1.9.22" - kotlin("plugin.jpa") version "1.9.22" + kotlin("jvm") version kotlinVersion + kotlin("plugin.spring") version kotlinVersion + kotlin("plugin.jpa") version kotlinVersion java } @@ -18,7 +19,7 @@ group = "de.grimsi" version = "2.0.0-SNAPSHOT" description = "gameyfin" -java.sourceCompatibility = JavaVersion.VERSION_21 +java.sourceCompatibility = JavaVersion.VERSION_22 configurations { compileOnly { @@ -34,6 +35,7 @@ repositories { } extra["hillaVersion"] = "2.5.6" +val springCloudVersion by extra("2023.0.2") dependencies { // Spring Boot & Kotlin @@ -54,6 +56,7 @@ dependencies { // Persistence implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.7") + implementation("org.springframework.cloud:spring-cloud-starter") // Development developmentOnly("org.springframework.boot:spring-boot-devtools") @@ -67,13 +70,17 @@ dependencies { dependencyManagement { imports { mavenBom("dev.hilla:hilla-bom:${property("hillaVersion")}") + mavenBom("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion") } } -tasks.withType { - kotlinOptions { - freeCompilerArgs += "-Xjsr305=strict" - jvmTarget = "21" +tasks.withType { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) + languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) + jvmTarget.set(JvmTarget.JVM_22) + progressiveMode.set(true) + freeCompilerArgs.add("-Xjsr305=strict") } } diff --git a/frontend/App.tsx b/frontend/App.tsx index 881701f..75cff10 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -6,9 +6,12 @@ import {themeNames} from "Frontend/theming/themes"; import {AuthProvider} from "Frontend/util/auth"; import {IconContext} from "@phosphor-icons/react"; import {Toaster} from "Frontend/@/components/ui/sonner"; +import client from "Frontend/generated/connect-client.default"; +import {ErrorHandlingMiddleware} from "Frontend/util/middleware"; export default function App() { const navigate = useNavigate(); + client.middlewares.push(ErrorHandlingMiddleware); return ( diff --git a/frontend/util/middleware.ts b/frontend/util/middleware.ts new file mode 100644 index 0000000..10ec4d9 --- /dev/null +++ b/frontend/util/middleware.ts @@ -0,0 +1,20 @@ +import {Middleware, MiddlewareContext, MiddlewareNext} from '@hilla/frontend'; +import {toast} from "sonner"; +import {getReasonPhrase} from "http-status-codes"; + +export const ErrorHandlingMiddleware: Middleware = async function( + context: MiddlewareContext, + next: MiddlewareNext +) { + const {endpoint, method} = context; + + let response: Response = await next(context); + if(!response.ok) { + //Ignore calls to UserEndpoint.getUserInfo since they are managed by Hilla and called on initial load + if(endpoint == "UserEndpoint" && method == "getUserInfo") return response; + + toast.error(`${getReasonPhrase(response.status)}`, {description: `${endpoint}.${method}`}) + } + + return response; +} \ No newline at end of file diff --git a/frontend/views/SetupView.tsx b/frontend/views/SetupView.tsx index c3f0135..132bc36 100644 --- a/frontend/views/SetupView.tsx +++ b/frontend/views/SetupView.tsx @@ -100,7 +100,7 @@ function SetupView() { password: values.password, email: values.email }); - toast("Setup finished", {description: "Have fun with Gameyfin!"}); + toast.success("Setup finished", {description: "Have fun with Gameyfin!"}); navigate('/login'); } catch (e) { alert("An error occurred while completing the setup. Please try again.") diff --git a/frontend/views/TestView.tsx b/frontend/views/TestView.tsx index 626c220..5a486ea 100644 --- a/frontend/views/TestView.tsx +++ b/frontend/views/TestView.tsx @@ -1,20 +1,40 @@ import {Link} from "react-router-dom"; import {Button} from "@nextui-org/react"; import {toast} from "sonner"; +import {SystemEndpoint} from "Frontend/generated/endpoints"; export default function TestView() { return ( -
+
Setup - +
+ + + +
+
); diff --git a/package-lock.json b/package-lock.json index 0f5471b..ea059af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "construct-style-sheets-polyfill": "3.1.0", "formik": "^2.4.5", "framer-motion": "^11.1.7", + "http-status-codes": "^2.3.0", "lit": "3.1.0", "next-themes": "^0.3.0", "react": "^18.2.0", @@ -9493,6 +9494,12 @@ "optional": true, "peer": true }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", diff --git a/package.json b/package.json index fb63af0..ec73147 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "construct-style-sheets-polyfill": "3.1.0", "formik": "^2.4.5", "framer-motion": "^11.1.7", + "http-status-codes": "^2.3.0", "lit": "3.1.0", "next-themes": "^0.3.0", "react": "^18.2.0", @@ -102,7 +103,8 @@ "@nextui-org/react": "$@nextui-org/react", "framer-motion": "$framer-motion", "@material-tailwind/react": "$@material-tailwind/react", - "sonner": "$sonner" + "sonner": "$sonner", + "http-status-codes": "$http-status-codes" }, "vaadin": { "dependencies": { @@ -146,6 +148,6 @@ "workbox-core": "7.0.0", "workbox-precaching": "7.0.0" }, - "hash": "3305a1ae01d771a26115b08f2597b5fbb020e6535692fd453407cba700f727ea" + "hash": "e078b3ecf381b7be4b804c8e2cd928faa9accb3412cfb55cfb649f9633cd1d41" } } diff --git a/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt b/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt index d75a4fa..8667e77 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/config/Roles.kt @@ -1,7 +1,16 @@ package de.grimsi.gameyfin.config enum class Roles(val roleName: String) { - SUPERADMIN("ROLE_SUPERADMIN"), - ADMIN("ROLE_ADMIN"), - USER("ROLE_USER") + SUPERADMIN(Names.SUPERADMIN), + ADMIN(Names.ADMIN), + USER(Names.USER); + + // necessary for the ability to use the Roles class in the @RolesAllowed annotation + class Names { + companion object { + const val SUPERADMIN = "ROLE_SUPERADMIN" + const val ADMIN = "ROLE_ADMIN" + const val USER = "ROLE_USER" + } + } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt index 2edb4b4..bfe6bef 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupDataLoader.kt @@ -29,13 +29,16 @@ class SetupDataLoader( log.info { "We will now set up some data..." } setupRoles() - //setupUsers() + setupUsers() log.info { "Setup completed..." } log.info { "Visit http://${InetAddress.getLocalHost().hostName}:${env.getProperty("server.port")}/setup to complete the setup" } } fun setupUsers() { + + log.info { "Setting up users..." } + val superadmin = User( username = "admin", password = "admin" @@ -49,6 +52,8 @@ class SetupDataLoader( ) userService.registerUser(user, Roles.USER) + + log.info { "User setup completed." } } fun setupRoles() { diff --git a/src/main/kotlin/de/grimsi/gameyfin/system/SystemEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/system/SystemEndpoint.kt new file mode 100644 index 0000000..5e0153c --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/system/SystemEndpoint.kt @@ -0,0 +1,16 @@ +package de.grimsi.gameyfin.system + +import de.grimsi.gameyfin.config.Roles +import dev.hilla.Endpoint +import jakarta.annotation.security.RolesAllowed + +@Endpoint +class SystemEndpoint( + private val systemService: SystemService +) { + + @RolesAllowed(Roles.Names.SUPERADMIN, Roles.Names.ADMIN) + fun restart() { + systemService.restart() + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/system/SystemService.kt b/src/main/kotlin/de/grimsi/gameyfin/system/SystemService.kt new file mode 100644 index 0000000..90ee884 --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/system/SystemService.kt @@ -0,0 +1,13 @@ +package de.grimsi.gameyfin.system + +import org.springframework.cloud.context.restart.RestartEndpoint +import org.springframework.stereotype.Service + +@Service +class SystemService( + private val restartEndpoint: RestartEndpoint, +) { + fun restart() { + restartEndpoint.restart() + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt index ee38448..da64547 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt @@ -12,8 +12,7 @@ import org.springframework.security.core.context.SecurityContextHolder @Endpoint class UserEndpoint( - private val userService: UserService, - private val roleService: RoleService, + private val userService: UserService ) { @PermitAll diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt b/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt index b2a802a..fbfa22c 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/Role.kt @@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotNull @Entity class Role( @NotNull + @Column(unique = true) var rolename: String, @Id diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt b/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt index e3b901c..ee8b3b9 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt @@ -8,23 +8,27 @@ import jakarta.validation.constraints.NotNull @Entity @Table(name = "users") class User( - @field:NotNull + @NotNull + @Column(unique = true) var username: String, @Id @GeneratedValue(strategy = GenerationType.AUTO) var id: Long? = null, - @field:NotNull + @NotNull var password: String, - @field:Nullable + @Nullable + @Column(unique = true) var email: String? = null, + var email_confirmed: Boolean = false, + var enabled: Boolean = true, @Embedded - @field:Nullable + @Nullable var avatar: Avatar? = null, @ManyToMany(fetch = FetchType.EAGER) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2d84231..a96ae5c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,6 +7,14 @@ server: session: tracking-modes: cookie +management: + endpoints.web.exposure.include: '*' + endpoint: + pause: + enabled: false + restart: + enabled: true + spring: # Workaround for https://github.com/vaadin/hilla/issues/842 devtools.restart.additional-exclude: dev/hilla/openapi.json