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 {
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<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "21"
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile> {
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")
}
}
+3
View File
@@ -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 (
<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,
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.")
+29 -9
View File
@@ -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 (
<div className="flex grow justify-center mt-12">
<div className="grow justify-center mt-12">
<div className="flex flex-col items-center gap-6">
<Link to="/setup">Setup</Link>
<Button onPress={
() => toast("Setup finished", {
description: "Have fun with Gameyfin!",
action: {
label: "OK",
onClick: () => console.log("Ok"),
}
})}>Toast</Button>
<div className="flex flex-row gap-4">
<Button onPress={
() => toast("Normal", {
description: "Description",
action: {
label: "OK",
onClick: () => {},
}
})}>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>
);
+7
View File
@@ -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",
+4 -2
View File
@@ -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"
}
}
@@ -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"
}
}
}
@@ -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() {
@@ -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
class UserEndpoint(
private val userService: UserService,
private val roleService: RoleService,
private val userService: UserService
) {
@PermitAll
@@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotNull
@Entity
class Role(
@NotNull
@Column(unique = true)
var rolename: String,
@Id
@@ -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)
+8
View File
@@ -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