Refine error handling in UI

Implement SystemEndpoint
This commit is contained in:
grimsi
2024-06-08 14:49:13 +02:00
parent b78e94b45e
commit 96c89662ec
16 changed files with 171 additions and 33 deletions
+24
View File
@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Generate Hilla sources" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="hillaGenerate" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>
+18 -11
View File
@@ -1,12 +1,13 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
id("org.springframework.boot") version "3.2.2" val kotlinVersion = "2.0.0"
id("io.spring.dependency-management") version "1.1.4" id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
id("dev.hilla") version "2.5.6" id("dev.hilla") version "2.5.6"
kotlin("jvm") version "1.9.22" kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version "1.9.22" kotlin("plugin.spring") version kotlinVersion
kotlin("plugin.jpa") version "1.9.22" kotlin("plugin.jpa") version kotlinVersion
java java
} }
@@ -18,7 +19,7 @@ group = "de.grimsi"
version = "2.0.0-SNAPSHOT" version = "2.0.0-SNAPSHOT"
description = "gameyfin" description = "gameyfin"
java.sourceCompatibility = JavaVersion.VERSION_21 java.sourceCompatibility = JavaVersion.VERSION_22
configurations { configurations {
compileOnly { compileOnly {
@@ -34,6 +35,7 @@ repositories {
} }
extra["hillaVersion"] = "2.5.6" extra["hillaVersion"] = "2.5.6"
val springCloudVersion by extra("2023.0.2")
dependencies { dependencies {
// Spring Boot & Kotlin // Spring Boot & Kotlin
@@ -54,6 +56,7 @@ dependencies {
// Persistence // Persistence
implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.7") implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.7")
implementation("org.springframework.cloud:spring-cloud-starter")
// Development // Development
developmentOnly("org.springframework.boot:spring-boot-devtools") developmentOnly("org.springframework.boot:spring-boot-devtools")
@@ -67,13 +70,17 @@ dependencies {
dependencyManagement { dependencyManagement {
imports { imports {
mavenBom("dev.hilla:hilla-bom:${property("hillaVersion")}") mavenBom("dev.hilla:hilla-bom:${property("hillaVersion")}")
mavenBom("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion")
} }
} }
tasks.withType<KotlinCompile> { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile> {
kotlinOptions { compilerOptions {
freeCompilerArgs += "-Xjsr305=strict" apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0)
jvmTarget = "21" languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0)
jvmTarget.set(JvmTarget.JVM_22)
progressiveMode.set(true)
freeCompilerArgs.add("-Xjsr305=strict")
} }
} }
+3
View File
@@ -6,9 +6,12 @@ import {themeNames} from "Frontend/theming/themes";
import {AuthProvider} from "Frontend/util/auth"; import {AuthProvider} from "Frontend/util/auth";
import {IconContext} from "@phosphor-icons/react"; import {IconContext} from "@phosphor-icons/react";
import {Toaster} from "Frontend/@/components/ui/sonner"; 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() { export default function App() {
const navigate = useNavigate(); const navigate = useNavigate();
client.middlewares.push(ErrorHandlingMiddleware);
return ( return (
<NextUIProvider className="size-full" navigate={navigate}> <NextUIProvider className="size-full" navigate={navigate}>
+20
View File
@@ -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;
}
+1 -1
View File
@@ -100,7 +100,7 @@ function SetupView() {
password: values.password, password: values.password,
email: values.email email: values.email
}); });
toast("Setup finished", {description: "Have fun with Gameyfin!"}); toast.success("Setup finished", {description: "Have fun with Gameyfin!"});
navigate('/login'); navigate('/login');
} catch (e) { } catch (e) {
alert("An error occurred while completing the setup. Please try again.") alert("An error occurred while completing the setup. Please try again.")
+29 -9
View File
@@ -1,20 +1,40 @@
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button} from "@nextui-org/react"; import {Button} from "@nextui-org/react";
import {toast} from "sonner"; import {toast} from "sonner";
import {SystemEndpoint} from "Frontend/generated/endpoints";
export default function TestView() { export default function TestView() {
return ( return (
<div className="flex grow justify-center mt-12"> <div className="grow justify-center mt-12">
<div className="flex flex-col items-center gap-6"> <div className="flex flex-col items-center gap-6">
<Link to="/setup">Setup</Link> <Link to="/setup">Setup</Link>
<Button onPress={ <div className="flex flex-row gap-4">
() => toast("Setup finished", { <Button onPress={
description: "Have fun with Gameyfin!", () => toast("Normal", {
action: { description: "Description",
label: "OK", action: {
onClick: () => console.log("Ok"), label: "OK",
} onClick: () => {},
})}>Toast</Button> }
})}>Toast (Normal)</Button>
<Button onPress={
() => toast.success("Success", {
description: "Description",
action: {
label: "OK",
onClick: () => {},
}
})}>Toast (Success)</Button>
<Button onPress={
() => toast.error("Error", {
description: "Description",
action: {
label: "OK",
onClick: () => {},
}
})}>Toast (Error)</Button>
</div>
<Button onPress={() => SystemEndpoint.restart()}>Restart</Button>
</div> </div>
</div> </div>
); );
+7
View File
@@ -36,6 +36,7 @@
"construct-style-sheets-polyfill": "3.1.0", "construct-style-sheets-polyfill": "3.1.0",
"formik": "^2.4.5", "formik": "^2.4.5",
"framer-motion": "^11.1.7", "framer-motion": "^11.1.7",
"http-status-codes": "^2.3.0",
"lit": "3.1.0", "lit": "3.1.0",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18.2.0", "react": "^18.2.0",
@@ -9493,6 +9494,12 @@
"optional": true, "optional": true,
"peer": 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": { "node_modules/idb": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+4 -2
View File
@@ -31,6 +31,7 @@
"construct-style-sheets-polyfill": "3.1.0", "construct-style-sheets-polyfill": "3.1.0",
"formik": "^2.4.5", "formik": "^2.4.5",
"framer-motion": "^11.1.7", "framer-motion": "^11.1.7",
"http-status-codes": "^2.3.0",
"lit": "3.1.0", "lit": "3.1.0",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18.2.0", "react": "^18.2.0",
@@ -102,7 +103,8 @@
"@nextui-org/react": "$@nextui-org/react", "@nextui-org/react": "$@nextui-org/react",
"framer-motion": "$framer-motion", "framer-motion": "$framer-motion",
"@material-tailwind/react": "$@material-tailwind/react", "@material-tailwind/react": "$@material-tailwind/react",
"sonner": "$sonner" "sonner": "$sonner",
"http-status-codes": "$http-status-codes"
}, },
"vaadin": { "vaadin": {
"dependencies": { "dependencies": {
@@ -146,6 +148,6 @@
"workbox-core": "7.0.0", "workbox-core": "7.0.0",
"workbox-precaching": "7.0.0" "workbox-precaching": "7.0.0"
}, },
"hash": "3305a1ae01d771a26115b08f2597b5fbb020e6535692fd453407cba700f727ea" "hash": "e078b3ecf381b7be4b804c8e2cd928faa9accb3412cfb55cfb649f9633cd1d41"
} }
} }
@@ -1,7 +1,16 @@
package de.grimsi.gameyfin.config package de.grimsi.gameyfin.config
enum class Roles(val roleName: String) { enum class Roles(val roleName: String) {
SUPERADMIN("ROLE_SUPERADMIN"), SUPERADMIN(Names.SUPERADMIN),
ADMIN("ROLE_ADMIN"), ADMIN(Names.ADMIN),
USER("ROLE_USER") 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"
}
}
} }
@@ -29,13 +29,16 @@ class SetupDataLoader(
log.info { "We will now set up some data..." } log.info { "We will now set up some data..." }
setupRoles() setupRoles()
//setupUsers() setupUsers()
log.info { "Setup completed..." } log.info { "Setup completed..." }
log.info { "Visit http://${InetAddress.getLocalHost().hostName}:${env.getProperty("server.port")}/setup to complete the setup" } log.info { "Visit http://${InetAddress.getLocalHost().hostName}:${env.getProperty("server.port")}/setup to complete the setup" }
} }
fun setupUsers() { fun setupUsers() {
log.info { "Setting up users..." }
val superadmin = User( val superadmin = User(
username = "admin", username = "admin",
password = "admin" password = "admin"
@@ -49,6 +52,8 @@ class SetupDataLoader(
) )
userService.registerUser(user, Roles.USER) userService.registerUser(user, Roles.USER)
log.info { "User setup completed." }
} }
fun setupRoles() { fun setupRoles() {
@@ -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()
}
}
@@ -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()
}
}
@@ -12,8 +12,7 @@ import org.springframework.security.core.context.SecurityContextHolder
@Endpoint @Endpoint
class UserEndpoint( class UserEndpoint(
private val userService: UserService, private val userService: UserService
private val roleService: RoleService,
) { ) {
@PermitAll @PermitAll
@@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotNull
@Entity @Entity
class Role( class Role(
@NotNull @NotNull
@Column(unique = true)
var rolename: String, var rolename: String,
@Id @Id
@@ -8,23 +8,27 @@ import jakarta.validation.constraints.NotNull
@Entity @Entity
@Table(name = "users") @Table(name = "users")
class User( class User(
@field:NotNull @NotNull
@Column(unique = true)
var username: String, var username: String,
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null, var id: Long? = null,
@field:NotNull @NotNull
var password: String, var password: String,
@field:Nullable @Nullable
@Column(unique = true)
var email: String? = null, var email: String? = null,
var email_confirmed: Boolean = false,
var enabled: Boolean = true, var enabled: Boolean = true,
@Embedded @Embedded
@field:Nullable @Nullable
var avatar: Avatar? = null, var avatar: Avatar? = null,
@ManyToMany(fetch = FetchType.EAGER) @ManyToMany(fetch = FetchType.EAGER)
+8
View File
@@ -7,6 +7,14 @@ server:
session: session:
tracking-modes: cookie tracking-modes: cookie
management:
endpoints.web.exposure.include: '*'
endpoint:
pause:
enabled: false
restart:
enabled: true
spring: spring:
# Workaround for https://github.com/vaadin/hilla/issues/842 # Workaround for https://github.com/vaadin/hilla/issues/842
devtools.restart.additional-exclude: dev/hilla/openapi.json devtools.restart.additional-exclude: dev/hilla/openapi.json