From 2c5d7d175b36aec9dc5fa0e844332cb6389b78d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horv=C3=A1th=20Istv=C3=A1n?= Date: Tue, 22 Oct 2024 15:55:29 +0200 Subject: [PATCH] Add error reporting --- .../errorlog/ErrorLogApiController.kt | 48 +++++++ .../component/errorlog/ErrorLogComponent.kt | 54 ++++++++ .../errorlog/ErrorLogComponentController.kt | 29 +++++ .../ErrorLogComponentEntityConfiguration.kt | 10 ++ .../component/errorlog/ErrorLogController.kt | 58 +++++++++ .../component/errorlog/ErrorLogEntity.kt | 121 ++++++++++++++++++ .../component/errorlog/ErrorLogRepository.kt | 26 ++++ .../component/errorlog/ErrorLogService.kt | 41 ++++++ .../hu/bme/sch/cmsch/config/AppConfig.kt | 3 + .../sch/cmsch/config/ComponentLoadConfig.kt | 9 ++ .../sch/cmsch/service/PermissionsService.kt | 29 +++++ .../config/application-env.properties | 1 + .../resources/config/application.properties | 2 + frontend/index.html | 85 +++++++----- frontend/src/util/errorBoundary.tsx | 5 +- 15 files changed, 488 insertions(+), 33 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogApiController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponent.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponentController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponentEntityConfiguration.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogEntity.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogRepository.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogService.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogApiController.kt new file mode 100644 index 000000000..2d87cf4c8 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogApiController.kt @@ -0,0 +1,48 @@ +package hu.bme.sch.cmsch.component.errorlog + +import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.util.getUserOrNull +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.Authentication +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.Optional + +@RestController() +@RequestMapping("/api") +class ErrorLogApiController( + private val errorLogComponent: Optional, + private val errorLogService: Optional +) { + + data class ErrorReportDto(val message: String?, val stack: String?, val userAgent: String?, val href: String?) + + @PostMapping("/error/submit") + @Transactional(isolation = Isolation.SERIALIZABLE) + fun submitError(auth: Authentication?, @RequestBody error: ErrorReportDto): ResponseEntity { + if (error.message == null && error.stack == null && error.userAgent == null && error.href == null) + return ResponseEntity.badRequest().build() + + val role = auth?.getUserOrNull()?.role ?: RoleType.GUEST + if (!errorLogComponent.map { it.minRole.isAvailableForRole(role) }.orElse(true)) + return ResponseEntity.status(HttpStatus.FORBIDDEN).build() + + errorLogService.ifPresent { + it.submit( + message = error.message ?: "", + stack = error.stack ?: "", + userAgent = error.userAgent ?: "", + href = error.href ?: "", + role = role + ) + } + + return ResponseEntity.ok().build() + } + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponent.kt new file mode 100644 index 000000000..529beaa54 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponent.kt @@ -0,0 +1,54 @@ +package hu.bme.sch.cmsch.component.errorlog + +import hu.bme.sch.cmsch.component.* +import hu.bme.sch.cmsch.component.app.ComponentSettingService +import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.service.ControlPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.core.env.Environment +import org.springframework.stereotype.Service + +@Service +@ConditionalOnProperty( + prefix = "hu.bme.sch.cmsch.component.load", + name = ["errorlog"], + havingValue = "true", + matchIfMissing = false +) +class ErrorLogComponent( + componentSettingService: ComponentSettingService, + env: Environment +) : ComponentBase( + "errorlog", + "/errorlog", + "Kliens hibaüzenetek", + ControlPermissions.PERMISSION_CONTROL_ERROR_LOG, + listOf(ErrorLogEntity::class), + componentSettingService, env +) { + + final override val allSettings by lazy { + listOf(errorLogGroup, menuDisplayName, minRole, receiveReports) + } + + val errorLogGroup = SettingProxy(componentSettingService, component, + "errorLogGroup", "", type = SettingType.COMPONENT_GROUP, persist = false, + fieldName = "Kliens hibák", + description = "" + ) + + final override val menuDisplayName = SettingProxy(componentSettingService, component, + "menuDisplayName", "Kliens hibák", serverSideOnly = true, + fieldName = "Menü neve", description = "Ez lesz a neve a menünek" + ) + + final override val minRole = MinRoleSettingProxy(componentSettingService, component, + "minRole", MinRoleSettingProxy.ALL_ROLES, minRoleToEdit = RoleType.SUPERUSER, + fieldName = "Jogosultságok", description = "Melyik roleok küldhetnek hibajelentéseket" + ) + + val receiveReports = SettingProxy(componentSettingService, component, "receiveReports", "true", + type = SettingType.BOOLEAN, fieldName = "Kliens hibajelentések fogadása", serverSideOnly = true + ) + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponentController.kt new file mode 100644 index 000000000..14c18d6f2 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponentController.kt @@ -0,0 +1,29 @@ +package hu.bme.sch.cmsch.component.errorlog + +import hu.bme.sch.cmsch.component.ComponentApiBase +import hu.bme.sch.cmsch.component.app.MenuService +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ControlPermissions.PERMISSION_CONTROL_ERROR_LOG +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/admin/control/component/errorlog") +@ConditionalOnBean(ErrorLogComponent::class) +class ErrorLogComponentController( + adminMenuService: AdminMenuService, + component: ErrorLogComponent, + menuService: MenuService, + auditLogService: AuditLogService +) : ComponentApiBase( + adminMenuService, + ErrorLogComponent::class.java, + component, + PERMISSION_CONTROL_ERROR_LOG, + "Kliens hibaüzenetek", + "Kliens hibaüzenetek testreszabása", + menuService = menuService, + auditLogService = auditLogService +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponentEntityConfiguration.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponentEntityConfiguration.kt new file mode 100644 index 000000000..eb4a3956e --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogComponentEntityConfiguration.kt @@ -0,0 +1,10 @@ +package hu.bme.sch.cmsch.component.errorlog + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.context.annotation.Configuration + +@Configuration +@ConditionalOnBean(ErrorLogComponent::class) +@EntityScan(basePackageClasses = [ErrorLogComponent::class]) +class ErrorLogComponentEntityConfiguration diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogController.kt new file mode 100644 index 000000000..f12aec468 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogController.kt @@ -0,0 +1,58 @@ +package hu.bme.sch.cmsch.component.errorlog + +import com.fasterxml.jackson.databind.ObjectMapper +import hu.bme.sch.cmsch.controller.admin.OneDeepEntityPage +import hu.bme.sch.cmsch.controller.admin.calculateSearchSettings +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ImplicitPermissions +import hu.bme.sch.cmsch.service.ImportService +import hu.bme.sch.cmsch.service.StaffPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import org.springframework.stereotype.Controller +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/admin/control/errorlog") +@ConditionalOnBean(ErrorLogComponent::class) +class ErrorLogController( + repo: ErrorLogRepository, + importService: ImportService, + adminMenuService: AdminMenuService, + component: ErrorLogComponent, + auditLog: AuditLogService, + objectMapper: ObjectMapper, + transactionManager: PlatformTransactionManager, + env: Environment +) : OneDeepEntityPage( + "errorlog", + ErrorLogEntity::class, ::ErrorLogEntity, + "Hibaüzenet", "Hibaüzenetek", + "A jelentett hibaüzenetek megtekintése", + + transactionManager, + repo, + importService, + adminMenuService, + component, + auditLog, + objectMapper, + env, + + showPermission = StaffPermissions.PERMISSION_SHOW_ERROR_LOG, + createPermission = ImplicitPermissions.PERMISSION_NOBODY, + editPermission = ImplicitPermissions.PERMISSION_NOBODY, + deletePermission = StaffPermissions.PERMISSION_DELETE_ERROR_LOG, + + createEnabled = false, + editEnabled = false, + deleteEnabled = true, + importEnabled = false, + exportEnabled = true, + + adminMenuIcon = "error", + adminMenuPriority = 1, + searchSettings = calculateSearchSettings(true) +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogEntity.kt new file mode 100644 index 000000000..e522b79d6 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogEntity.kt @@ -0,0 +1,121 @@ +package hu.bme.sch.cmsch.component.errorlog + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.admin.* +import hu.bme.sch.cmsch.component.EntityConfig +import hu.bme.sch.cmsch.dto.Edit +import hu.bme.sch.cmsch.dto.FullDetails +import hu.bme.sch.cmsch.dto.Preview +import hu.bme.sch.cmsch.model.ManagedEntity +import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.service.StaffPermissions +import jakarta.persistence.* +import org.hibernate.Hibernate +import org.hibernate.annotations.JdbcTypeCode +import org.hibernate.type.SqlTypes +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment + + +@Entity +@Table( + name = "errorLog", + uniqueConstraints = [UniqueConstraint(columnNames = ["message", "stack", "userAgent", "href", "role"])] +) +@ConditionalOnBean(ErrorLogComponent::class) +data class ErrorLogEntity( + @Id + @GeneratedValue + @field:JsonView(value = [Edit::class]) + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + override var id: Int = 0, + + @field:JsonView(value = [Edit::class, Preview::class, FullDetails::class]) + @Column(nullable = false, length = 2048) + @property:GenerateInput(maxLength = 512, type = INPUT_TYPE_BLOCK_TEXT, order = 1, label = "Hiba") + @property:GenerateOverview(columnName = "Hiba", order = 1, useForSearch = true) + @property:ImportFormat + var message: String = "", + + @field:JsonView(value = [Edit::class, Preview::class, FullDetails::class]) + @Column(nullable = false, length = 25000) + @property:GenerateInput(maxLength = 2048, type = INPUT_TYPE_BLOCK_TEXT, order = 2, label = "Stacktrace") + @property:GenerateOverview(visible = false, columnName = "Stacktrace", order = 2, useForSearch = true) + @property:ImportFormat + var stack: String = "", + + @field:JsonView(value = [Edit::class, Preview::class, FullDetails::class]) + @Column(nullable = false, length = 1024) + @property:GenerateInput(maxLength = 512, type = INPUT_TYPE_BLOCK_TEXT, order = 3, label = "User Agent") + @property:GenerateOverview(visible = false, columnName = "User Agent", order = 3, useForSearch = true) + @property:ImportFormat + var userAgent: String = "", + + @field:JsonView(value = [Edit::class, Preview::class, FullDetails::class]) + @Column(nullable = false, length = 2048) + @property:GenerateInput(maxLength = 512, order = 4, label = "href") + @property:GenerateOverview(columnName = "href", order = 4, useForSearch = true) + @property:ImportFormat + var href: String = "", + + @field:JsonView(value = [Edit::class, FullDetails::class]) + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.VARCHAR) + @property:GenerateInput( + type = INPUT_TYPE_BLOCK_SELECT, order = 5, label = "Jogkör", + source = ["GUEST", "BASIC", "ATTENDEE", "PRIVILEGED", "STAFF", "ADMIN", "SUPERUSER"], + minimumRole = RoleType.ADMIN, note = "BASIC = belépett, STAFF = rendező, ADMIN = minden jog" + ) + @property:GenerateOverview(visible = true, columnName = "Jelentő jogköre", order = 5) + @property:ImportFormat + var role: RoleType = RoleType.GUEST, + + @field:JsonView(value = [Edit::class, Preview::class, FullDetails::class]) + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 6, label = "Ennyiszer jelentve") + @property:GenerateOverview( + visible = true, + columnName = "Ennyiszer jelentve", + order = 6, + renderer = OVERVIEW_TYPE_NUMBER + ) + @property:ImportFormat + var count: Long = 1, + + @field:JsonView(value = [Edit::class, Preview::class, FullDetails::class]) + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_DATE, order = 7, label = "Utoljára jelentve") + @property:GenerateOverview( + visible = true, + columnName = "Utoljára jelentve", + order = 7, + renderer = OVERVIEW_TYPE_DATE + ) + @property:ImportFormat + var lastReportedAt: Long = 0, +) : ManagedEntity { + + override fun getEntityConfig(env: Environment) = EntityConfig( + name = "ErrorLogEntity", + view = "control/errorlog", + showPermission = StaffPermissions.PERMISSION_SHOW_ERROR_LOG + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + other as ErrorLogEntity + + return id != 0 && id == other.id + } + + override fun hashCode(): Int = javaClass.hashCode() + + @Override + override fun toString(): String { + return this::class.simpleName + "(id = $id )" + } + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogRepository.kt new file mode 100644 index 000000000..ddfd4a2c2 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogRepository.kt @@ -0,0 +1,26 @@ +package hu.bme.sch.cmsch.component.errorlog + +import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.repository.EntityPageDataSource +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository +import java.util.Optional + +@Repository +@ConditionalOnBean(ErrorLogComponent::class) +interface ErrorLogRepository : CrudRepository, EntityPageDataSource { + + @Query("select e from ErrorLogEntity e order by e.lastReportedAt desc") + override fun findAll(): List + + fun findByMessageAndStackAndUserAgentAndHrefAndRole( + message: String, + stack: String, + userAgent: String, + href: String, + role: RoleType + ): Optional + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogService.kt new file mode 100644 index 000000000..039fa9aa7 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/errorlog/ErrorLogService.kt @@ -0,0 +1,41 @@ +package hu.bme.sch.cmsch.component.errorlog + +import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.service.TimeService +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Transactional +import kotlin.jvm.optionals.getOrNull + +@Service +@ConditionalOnBean(ErrorLogComponent::class) +class ErrorLogService( + private val errorLogComponent: ErrorLogComponent, + private val errorLogRepository: ErrorLogRepository, + private val clock: TimeService +) { + + @Transactional(isolation = Isolation.SERIALIZABLE) + fun submit(message: String, stack: String, userAgent: String, href: String, role: RoleType) { + if (!errorLogComponent.receiveReports.isValueTrue()) return + + val existingLog = errorLogRepository + .findByMessageAndStackAndUserAgentAndHrefAndRole(message, stack, userAgent, href, role).getOrNull() + + val logToSave = existingLog ?: ErrorLogEntity( + message = message, + stack = stack, + userAgent = userAgent, + href = href, + role = role, + ) + logToSave.apply { + count = count + 1 + lastReportedAt = clock.getTimeInSeconds() + } + + errorLogRepository.save(logToSave) + } + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/AppConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/AppConfig.kt index 6ab7e7fc2..5c2683d59 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/AppConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/AppConfig.kt @@ -2,6 +2,8 @@ package hu.bme.sch.cmsch.config import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.convert.TypeDescriptor @@ -24,6 +26,7 @@ class AppConfig { @Bean fun objectMapper(): ObjectMapper { val objectMapper = ObjectMapper() + objectMapper.registerKotlinModule() objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) return objectMapper } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt index 7e2a4d405..bc9521356 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt @@ -32,6 +32,15 @@ data class ComponentLoadConfig @ConstructorBinding constructor( var task: Boolean, var team: Boolean, var token: Boolean, + var accessKeys: Boolean, + var conference: Boolean, + var email: Boolean, + var errorlog: Boolean, + var gallery: Boolean, + var messaging: Boolean, + var proto: Boolean, + var pushnotification: Boolean, + var sheets: Boolean, ) { diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt index 62df92ca0..20f9d089d 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt @@ -11,6 +11,7 @@ import hu.bme.sch.cmsch.component.conference.ConferenceComponent import hu.bme.sch.cmsch.component.countdown.CountdownComponent import hu.bme.sch.cmsch.component.debt.DebtComponent import hu.bme.sch.cmsch.component.email.EmailComponent +import hu.bme.sch.cmsch.component.errorlog.ErrorLogComponent import hu.bme.sch.cmsch.component.event.EventComponent import hu.bme.sch.cmsch.component.form.FormComponent import hu.bme.sch.cmsch.component.gallery.GalleryComponent @@ -137,6 +138,13 @@ object ControlPermissions : PermissionGroup { component = EventComponent::class ) + val PERMISSION_CONTROL_ERROR_LOG = PermissionValidator( + "ERROR_LOG_CONTROL", + "Kliens hibaüzenetek komponens testreszabása", + readOnly = false, + component = ErrorLogComponent::class + ) + val PERMISSION_CONTROL_GALLERY = PermissionValidator( "GALLERY_CONTROL", "Galéria komponens testreszabása", @@ -427,6 +435,7 @@ object ControlPermissions : PermissionGroup { PERMISSION_CONTROL_NEWS, PERMISSION_CONTROL_TASKS, PERMISSION_CONTROL_EVENTS, + PERMISSION_CONTROL_ERROR_LOG, PERMISSION_CONTROL_GALLERY, PERMISSION_CONTROL_DEBTS, PERMISSION_CONTROL_RIDDLE, @@ -806,6 +815,23 @@ object StaffPermissions : PermissionGroup { component = EventComponent::class ) + /// ErrorLogComponent + + val PERMISSION_SHOW_ERROR_LOG = PermissionValidator( + "ERROR_LOG_SHOW", + "Hibaüzenetek megtekintése", + readOnly = true, + component = ErrorLogComponent::class + ) + + val PERMISSION_DELETE_ERROR_LOG = PermissionValidator( + "ERROR_LOG_DELETE", + "Hibaüzenetek törlése", + readOnly = false, + component = ErrorLogComponent::class + ) + + /// GalleryComponent val PERMISSION_SHOW_GALLERY = PermissionValidator( @@ -1608,6 +1634,9 @@ object StaffPermissions : PermissionGroup { PERMISSION_CREATE_EVENTS, PERMISSION_DELETE_EVENTS, + PERMISSION_SHOW_ERROR_LOG, + PERMISSION_DELETE_ERROR_LOG, + PERMISSION_SHOW_GALLERY, PERMISSION_EDIT_GALLERY, PERMISSION_CREATE_GALLERY, diff --git a/backend/src/main/resources/config/application-env.properties b/backend/src/main/resources/config/application-env.properties index 6288b36f7..4b72d0c35 100644 --- a/backend/src/main/resources/config/application-env.properties +++ b/backend/src/main/resources/config/application-env.properties @@ -20,6 +20,7 @@ hu.bme.sch.cmsch.component.load.messaging=${LOAD_MESSAGING:false} hu.bme.sch.cmsch.component.load.news=${LOAD_NEWS:false} hu.bme.sch.cmsch.component.load.profile=${LOAD_PROFILE:true} hu.bme.sch.cmsch.component.load.proto=${LOAD_PROTO:true} +hu.bme.sch.cmsch.component.load.errorlog=${LOAD_ERRORLOG:true} hu.bme.sch.cmsch.component.load.pushnotification=${LOAD_PUSHNOTIFICATION:false} hu.bme.sch.cmsch.component.load.qrFight=${LOAD_QRFIGHT:false} hu.bme.sch.cmsch.component.load.race=${LOAD_RACE:false} diff --git a/backend/src/main/resources/config/application.properties b/backend/src/main/resources/config/application.properties index 97908e5ad..abecf3556 100644 --- a/backend/src/main/resources/config/application.properties +++ b/backend/src/main/resources/config/application.properties @@ -66,6 +66,7 @@ hu.bme.sch.cmsch.component.load.countdown=true hu.bme.sch.cmsch.component.load.debt=true hu.bme.sch.cmsch.component.load.email=true hu.bme.sch.cmsch.component.load.event=true +hu.bme.sch.cmsch.component.load.errorlog=true hu.bme.sch.cmsch.component.load.form=true hu.bme.sch.cmsch.component.load.gallery=false hu.bme.sch.cmsch.component.load.groupselection=true @@ -124,6 +125,7 @@ hu.bme.sch.cmsch.app.style.priority=152 hu.bme.sch.cmsch.app.function.priority=153 hu.bme.sch.cmsch.app.dev.priority=154 hu.bme.sch.cmsch.app.data.priority=155 +hu.bme.sch.cmsch.errorlog.priority=156 #hu.bme.sch.cmsch.startup.profile-qr-enabled=true diff --git a/frontend/index.html b/frontend/index.html index 194c29951..f1381bd01 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,38 +1,59 @@ - - - - %VITE_NAME% - - - - - - - - - - - - - - + + + + %VITE_NAME% + + + + + + + + + + + + + + - - - - -
- - + function processAndReportError(error) { + if (typeof error !== 'object') { + reportError(JSON.stringify(error), null) + } else { + const message = error.message || null + const stack = error.stack || null + reportError(message, stack) + } + } + + window.addEventListener('unhandledrejection', (error) => processAndReportError(error.reason)) + window.addEventListener('error', (error) => processAndReportError(error.error)) + + + + + + +
+ + diff --git a/frontend/src/util/errorBoundary.tsx b/frontend/src/util/errorBoundary.tsx index 41f91bcdd..3196a2391 100644 --- a/frontend/src/util/errorBoundary.tsx +++ b/frontend/src/util/errorBoundary.tsx @@ -23,8 +23,11 @@ export class ErrorBoundary extends React.Component { } } - componentDidCatch(_: Error, errorInfo: ErrorInfo) { + componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error(errorInfo) + + const win: any = window + win.processAndReportError?.call(error.message, error.stack) } render() {