diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index d6bfc1c13..be8d39faa 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2") implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-aop") 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") @@ -130,4 +131,4 @@ tasks.getByName(" tasks.withType { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/CMSchApplication.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/CMSchApplication.kt index 5bbedda09..aa121cc3b 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/CMSchApplication.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/CMSchApplication.kt @@ -7,12 +7,14 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.persistence.autoconfigure.EntityScan import org.springframework.boot.runApplication +import org.springframework.resilience.annotation.EnableResilientMethods @SpringBootApplication @EnableConfigurationProperties(value = [ComponentLoadConfig::class, StartupPropertyConfig::class]) @EntityScan(basePackages = ["hu.bme.sch.cmsch.model"], basePackageClasses = [ApplicationComponent::class]) +@EnableResilientMethods class CMSchApplication fun main(args: Array) { runApplication(*args) -} +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationEntity.kt index f466ccb95..bc685816f 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationEntity.kt @@ -1,5 +1,6 @@ package hu.bme.sch.cmsch.component.location +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonView import hu.bme.sch.cmsch.admin.* import hu.bme.sch.cmsch.component.EntityConfig @@ -31,6 +32,11 @@ data class LocationEntity( @property:GenerateOverview(renderer = OverviewType.ID, columnName = "ID", order = -1) override var id: Int = 0, + @Column(nullable = false, unique = true) + @JsonIgnore + @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) + var token: String = "", + @Column(nullable = false) @field:JsonView(value = [ Edit::class, FullDetails::class ]) @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) @@ -156,4 +162,4 @@ data class LocationEntity( return this.copy() } -} +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationRepository.kt index 339255111..412163072 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationRepository.kt @@ -12,5 +12,16 @@ interface LocationRepository : CrudRepository, EntityPageDataSource { fun findByUserId(userId: Int): Optional + + fun findByToken(token: String): Optional + + fun findAllByGroupNameOrderByGroupNameAsc(groupName: String): List + + fun findAllByTimestampGreaterThan(timestamp: Long): List + + fun findAllByTimestampLessThan(timestamp: Long): List + + fun deleteByUserId(userId: Int) + override fun findAll(): MutableIterable -} +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationService.kt index db0b56455..bf412c4e4 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/location/LocationService.kt @@ -2,16 +2,17 @@ package hu.bme.sch.cmsch.component.location import hu.bme.sch.cmsch.config.StartupPropertyConfig import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.model.UserEntity import hu.bme.sch.cmsch.repository.EntityPageDataSource import hu.bme.sch.cmsch.repository.UserRepository import hu.bme.sch.cmsch.service.StaffPermissions import hu.bme.sch.cmsch.service.TimeService -import hu.bme.sch.cmsch.util.transaction +import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import org.springframework.transaction.PlatformTransactionManager import java.util.* -import java.util.concurrent.ConcurrentHashMap import kotlin.jvm.optionals.getOrNull @Service @@ -21,66 +22,75 @@ class LocationService( private val userRepository: UserRepository, private val startupPropertyConfig: StartupPropertyConfig, private val waypointRepository: WaypointRepository, + private val locationRepository: LocationRepository, private val locationComponent: LocationComponent, private val transactionManager: PlatformTransactionManager ) : EntityPageDataSource { - private val tokenToLocationMapping = ConcurrentHashMap() + private val log = LoggerFactory.getLogger(javaClass) fun pushLocation(locationDto: LocationDto): LocationResponse { - if (!tokenToLocationMapping.containsKey(locationDto.token)) { - val user = transactionManager.transaction(readOnly = true) { - userRepository.findByCmschId(startupPropertyConfig.profileQrPrefix + locationDto.token) - } - if (user.isPresent) { - val userEntity = user.get() - if (userEntity.role.value >= RoleType.STAFF.value) { - tokenToLocationMapping[locationDto.token] = - LocationEntity( - id = 0, - userId = userEntity.id, - userName = userEntity.fullName, - alias = userEntity.alias, - groupName = userEntity.groupName, - markerColor = resolveColor(userEntity.groupName), - broadcast = locationDto.broadcastEnabled - && userEntity.hasPermission(StaffPermissions.PERMISSION_BROADCAST_LOCATION.permissionString) - ) - } else { - return LocationResponse("jogosulatlan", "n/a") - } - } else { - return LocationResponse("nem jogosult", "n/a") - } + val user = userRepository.findByCmschId(startupPropertyConfig.profileQrPrefix + locationDto.token) + if (user.isEmpty) { + return LocationResponse("nem jogosult", "n/a") } - val entity = tokenToLocationMapping[locationDto.token]?.let { - it.longitude = locationDto.longitude - it.latitude = locationDto.latitude - it.altitude = locationDto.altitude - it.accuracy = locationDto.accuracy - it.speed = locationDto.speed - it.altitudeAccuracy = locationDto.altitudeAccuracy - it.heading = locationDto.heading - it.timestamp = clock.getTimeInSeconds() - it.broadcast = locationDto.broadcastEnabled && shareLocationAllowed(locationDto.token) - return@let it + val userEntity = user.get() + if (userEntity.role.value < RoleType.STAFF.value) { + return LocationResponse("jogosulatlan", "n/a") } - return if (entity != null) { - LocationResponse(if (entity.broadcast) "OK" else "BROADCAST", entity.groupName) + val existingByUserId = locationRepository.findByUserId(userEntity.id) + val existingByToken = locationRepository.findByToken(locationDto.token) + val entity = if (existingByUserId.isPresent) { + existingByUserId.get() + } else if (existingByToken.isPresent) { + existingByToken.get() } else { - LocationResponse("nem található", "n/a") + LocationEntity( + id = 0, + token = locationDto.token, + userId = userEntity.id, + userName = userEntity.fullName, + alias = userEntity.alias, + groupName = userEntity.groupName, + markerColor = resolveColor(userEntity.groupName), + broadcast = locationDto.broadcastEnabled + && userEntity.hasPermission(StaffPermissions.PERMISSION_BROADCAST_LOCATION.permissionString) + ) } + + // Refresh user metadata for existing entities to avoid stale data + entity.token = locationDto.token + entity.userId = userEntity.id + entity.userName = userEntity.fullName + entity.alias = userEntity.alias + entity.groupName = userEntity.groupName + entity.markerColor = resolveColor(userEntity.groupName) + + entity.longitude = locationDto.longitude + entity.latitude = locationDto.latitude + entity.altitude = locationDto.altitude + entity.accuracy = locationDto.accuracy + entity.speed = locationDto.speed + entity.altitudeAccuracy = locationDto.altitudeAccuracy + entity.heading = locationDto.heading + entity.timestamp = clock.getTimeInSeconds() + entity.broadcast = locationDto.broadcastEnabled && shareLocationAllowed(userEntity) + + locationRepository.save(entity) + return LocationResponse(if (entity.broadcast) "OK" else "BROADCAST", entity.groupName) } private fun shareLocationAllowed(token: String): Boolean { - val user = transactionManager.transaction(readOnly = true) { - userRepository.findByCmschId(startupPropertyConfig.profileQrPrefix + token) - } + val user = userRepository.findByCmschId(startupPropertyConfig.profileQrPrefix + token) return user.getOrNull()?.hasPermission(StaffPermissions.PERMISSION_BROADCAST_LOCATION.permissionString) ?: false } + private fun shareLocationAllowed(user: UserEntity): Boolean { + return user.hasPermission(StaffPermissions.PERMISSION_BROADCAST_LOCATION.permissionString) + } + private fun resolveColor(groupName: String): String { return when (groupName) { locationComponent.blackGroupName -> { "#000000" } @@ -99,30 +109,33 @@ class LocationService( } fun findAllLocation(): MutableList { - return tokenToLocationMapping.values.toList() - .sortedBy { it.groupName }.toMutableList() + return locationRepository.findAll() + .toList() + .sortedBy { it.groupName } + .toMutableList() } fun clean() { - return tokenToLocationMapping.clear() + locationRepository.deleteAll() } fun refresh() { - return tokenToLocationMapping.keys() - .asSequence() - .forEach { token -> - tokenToLocationMapping[token]?.let { - val user = userRepository.findByCmschId(startupPropertyConfig.profileQrPrefix + token) - it.userId = user.get().id - it.userName = user.get().fullName - it.alias = user.get().alias - it.groupName = user.get().groupName - } - } + locationRepository.findAll().forEach { location -> + val user = userRepository.findByCmschId(startupPropertyConfig.profileQrPrefix + location.token) + user.ifPresent { userEntity -> + location.userId = userEntity.id + location.userName = userEntity.fullName + location.alias = userEntity.alias + location.groupName = userEntity.groupName + location.markerColor = resolveColor(userEntity.groupName) + locationRepository.save(location) + } + } } fun findLocationsOfGroup(groupId: Int): List { - return tokenToLocationMapping.values.filter { it.id == groupId } + return locationRepository.findAll() + .filter { it.userId == groupId } } fun findLocationsOfGroupName(group: String): List { @@ -130,23 +143,25 @@ class LocationService( val visibilityDuration = locationComponent.visibleDuration val currentTime = clock.getTimeInSeconds() - locations.addAll(tokenToLocationMapping.values + locations.addAll(locationRepository.findAll() .filter { it.groupName == group || it.broadcast } .filter { it.timestamp + visibilityDuration > currentTime } - .map { MapMarker( - displayName = mapDisplayName(it), - longitude = it.longitude, - latitude = it.latitude, - altitude = it.altitude, - accuracy = it.accuracy, - altitudeAccuracy = it.altitudeAccuracy, - heading = it.heading, - speed = it.speed, - timestamp = it.timestamp, - markerShape = it.markerShape, - markerColor = it.markerColor, - description = it.description, - ) } + .map { + MapMarker( + displayName = mapDisplayName(it), + longitude = it.longitude, + latitude = it.latitude, + altitude = it.altitude, + accuracy = it.accuracy, + altitudeAccuracy = it.altitudeAccuracy, + heading = it.heading, + speed = it.speed, + timestamp = it.timestamp, + markerShape = it.markerShape, + markerColor = it.markerColor, + description = it.description, + ) + } ) locations.addAll(waypointRepository.findAll().map { it.toMapMarker() }) return locations @@ -181,38 +196,42 @@ class LocationService( } fun getRecentLocations(): List { - val range = clock.getTimeInSeconds() + 600 - return tokenToLocationMapping.values.filter { it.timestamp < range } + val range = clock.getTimeInSeconds() - 600 + return locationRepository.findAllByTimestampGreaterThan(range) + } + + @Scheduled(fixedRate = 60000) + fun cleanupStaleLocations() { + val visibilityDuration = locationComponent.visibleDuration + val cutoff = clock.getTimeInSeconds() - visibilityDuration + val staleLocations = locationRepository.findAllByTimestampLessThan(cutoff) + + if (staleLocations.isNotEmpty()) { + log.info("Cleaning up {} stale locations", staleLocations.size) + locationRepository.deleteAll(staleLocations) + } } override fun findAll() = findAllLocation() - override fun count() = findAllLocation().size.toLong() + override fun count() = locationRepository.count() override fun deleteAll() = clean() override fun saveAll(entities: MutableIterable): MutableIterable { - return entities.map { save(it) }.toMutableList() + return locationRepository.saveAll(entities) } override fun save(entity: S): S { - val keyToUpdate = tokenToLocationMapping.entries.find { it.value.id == entity.id }?.key - if (keyToUpdate != null) - tokenToLocationMapping[keyToUpdate] = entity - return entity + return locationRepository.save(entity) } override fun delete(entity: LocationEntity) { - val keyToRemove = tokenToLocationMapping.entries.find { it.value.id == entity.id }?.key - if (keyToRemove != null) - tokenToLocationMapping.remove(keyToRemove) + locationRepository.delete(entity) } override fun findById(id: Int): Optional { - return Optional.ofNullable(tokenToLocationMapping.entries - .filter { it.value.id == id } - .map { it.value } - .firstOrNull()) + return locationRepository.findById(id) } -} +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/login/InternalIdLocks.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/login/InternalIdLocks.kt deleted file mode 100644 index 48ce852ee..000000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/login/InternalIdLocks.kt +++ /dev/null @@ -1,26 +0,0 @@ -package hu.bme.sch.cmsch.component.login - -import java.lang.ref.WeakReference -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.locks.ReentrantLock - -class InternalIdLocks { - - private val locks = ConcurrentHashMap>() - - private fun getLockForKey(key: String): ReentrantLock { - locks.entries.removeIf { it.value.get() == null } - - val newLockRef = WeakReference(ReentrantLock()) - val existingLockRef = locks.putIfAbsent(key, newLockRef) - - return existingLockRef?.get() ?: newLockRef.get()!! - } - - fun lockForKey(key: String): ReentrantLock { - val lockForKey = getLockForKey(key) - lockForKey.lock() - return lockForKey - } - -} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/login/LoginService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/login/LoginService.kt index c2b78000a..055409c13 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/login/LoginService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/login/LoginService.kt @@ -18,6 +18,7 @@ import hu.bme.sch.cmsch.service.UserProfileGeneratorService import hu.bme.sch.cmsch.service.UserService import hu.bme.sch.cmsch.util.transaction import org.slf4j.LoggerFactory +import org.springframework.dao.DataIntegrityViolationException import org.springframework.stereotype.Service import org.springframework.transaction.PlatformTransactionManager import java.util.* @@ -39,88 +40,112 @@ class LoginService( ) { private val log = LoggerFactory.getLogger(javaClass) - private val userLocks = InternalIdLocks() + + private fun saveUserWithConflictRecovery(user: UserEntity): UserEntity { + return try { + val savedUser = users.save(user) + savedUser + } catch (e: DataIntegrityViolationException) { + log.warn("Conflict saving user with internalId ${user.internalId}, attempting recovery", e) + val canonical = users.findByInternalId(user.internalId) + .or { if (user.email.isNotBlank()) users.findByEmailIgnoreCase(user.email) else Optional.empty() } + .orElseThrow { IllegalStateException("Cannot resolve conflicting user after constraint violation") } + + canonical.internalId = user.internalId + if (user.email.isNotBlank()) { + canonical.email = user.email + } + canonical.fullName = user.fullName + canonical.neptun = user.neptun + canonical.role = user.role + canonical.groupName = user.groupName + canonical.group = user.group + canonical.guild = user.guild + canonical.major = user.major + canonical.permissions = user.permissions + canonical.profilePicture = user.profilePicture + canonical.profileTopMessage = user.profileTopMessage + canonical.detailsImported = user.detailsImported + canonical.provider = user.provider + canonical.unitScopes = user.unitScopes + canonical.alias = user.alias + canonical.cmschId = user.cmschId + + val savedCanonical = users.save(canonical) + savedCanonical + } + } fun fetchUserEntity(profile: ProfileResponse): UserEntity { - val lock = userLocks.lockForKey(profile.internalId) - try { - var user: UserEntity - val existingByInternalId = users.findByInternalId(profile.internalId) - if (existingByInternalId.isPresent) { - user = existingByInternalId.get() - log.info("Logging in with existing user ${user.fullName} as authsch user") + var user: UserEntity + val existingByInternalId = users.findByInternalId(profile.internalId) + if (existingByInternalId.isPresent) { + user = existingByInternalId.get() + log.info("Logging in with existing user ${user.fullName} as authsch user") + } else { + val existingByEmail = + if (!profile.email.isNullOrBlank()) users.findByEmailIgnoreCase(profile.email!!) else Optional.empty() + if (existingByEmail.isPresent) { + user = existingByEmail.get() + log.info("Logging in with existing user ${user.fullName} (found by email) as authsch user, internalId updated from ${user.internalId} to ${profile.internalId}") + user.internalId = profile.internalId } else { - val existingByEmail = - if (!profile.email.isNullOrBlank()) users.findByEmailIgnoreCase(profile.email!!) else Optional.empty() - if (existingByEmail.isPresent) { - user = existingByEmail.get() - log.info("Logging in with existing user ${user.fullName} (found by email) as authsch user, internalId updated from ${user.internalId} to ${profile.internalId}") - user.internalId = profile.internalId - } else { - user = UserEntity( - 0, - profile.internalId, - profile.neptun ?: "N/A", - "", - (profile.surname ?: "") + " " + (profile.givenName ?: ""), - "", - profile.email ?: "", - RoleType.BASIC, - groupName = "", group = null, - guild = GuildType.UNKNOWN, major = MajorType.UNKNOWN, - provider = AUTHSCH - ) - log.info("Logging in with new user ${user.fullName} internalId: ${user.internalId} as authsch user") - } + user = UserEntity( + 0, + profile.internalId, + profile.neptun ?: "N/A", + "", + (profile.surname ?: "") + " " + (profile.givenName ?: ""), + "", + profile.email ?: "", + RoleType.BASIC, + groupName = "", group = null, + guild = GuildType.UNKNOWN, major = MajorType.UNKNOWN, + provider = AUTHSCH + ) + log.info("Logging in with new user ${user.fullName} internalId: ${user.internalId} as authsch user") } - transactionManager.transaction(readOnly = true) { updateFieldsForAuthsch(user, profile) } - transactionManager.transaction(readOnly = false) { users.save(user) } - adminMenuService.invalidateUser(user.internalId) - return user - } finally { - lock.unlock() } + transactionManager.transaction(readOnly = true) { updateFieldsForAuthsch(user, profile) } + val savedUser = transactionManager.transaction(readOnly = false) { saveUserWithConflictRecovery(user) } + adminMenuService.invalidateUser(savedUser.internalId) + return savedUser } fun fetchGoogleUserEntity(profile: GoogleUserInfoResponse): UserEntity { - val lock = userLocks.lockForKey(profile.internalId) - try { - var user: UserEntity - val existingByInternalId = users.findByInternalId(profile.internalId) - if (existingByInternalId.isPresent) { - user = existingByInternalId.get() - log.info("Logging in with existing user ${user.fullName} as google user") + var user: UserEntity + val existingByInternalId = users.findByInternalId(profile.internalId) + if (existingByInternalId.isPresent) { + user = existingByInternalId.get() + log.info("Logging in with existing user ${user.fullName} as google user") + } else { + val existingByEmail = users.findByEmailIgnoreCase(profile.email) + if (existingByEmail.isPresent) { + user = existingByEmail.get() + log.info("Logging in with existing user ${user.fullName} (found by email) as google user, internalId updated from ${user.internalId} to ${profile.internalId}") + user.internalId = profile.internalId } else { - val existingByEmail = users.findByEmailIgnoreCase(profile.email) - if (existingByEmail.isPresent) { - user = existingByEmail.get() - log.info("Logging in with existing user ${user.fullName} (found by email) as google user, internalId updated from ${user.internalId} to ${profile.internalId}") - user.internalId = profile.internalId - } else { - user = UserEntity( - 0, - profile.internalId.take(254), - "N/A", - "", - "${profile.familyName} ${profile.givenName}".take(254), - "", - profile.email.take(254), - RoleType.BASIC, - groupName = "", group = null, - guild = GuildType.UNKNOWN, major = MajorType.UNKNOWN, - provider = GOOGLE, - profilePicture = profile.picture.take(254) - ) - log.info("Logging in with new user ${user.fullName} internalId: ${user.internalId} as google user profile picture: ${profile.picture}") - } + user = UserEntity( + 0, + profile.internalId.take(254), + "N/A", + "", + "${profile.familyName} ${profile.givenName}".take(254), + "", + profile.email.take(254), + RoleType.BASIC, + groupName = "", group = null, + guild = GuildType.UNKNOWN, major = MajorType.UNKNOWN, + provider = GOOGLE, + profilePicture = profile.picture.take(254) + ) + log.info("Logging in with new user ${user.fullName} internalId: ${user.internalId} as google user profile picture: ${profile.picture}") } - transactionManager.transaction(readOnly = true) { updateFieldsForGoogle(user) } - transactionManager.transaction(readOnly = false) { users.save(user) } - adminMenuService.invalidateUser(user.internalId) - return user - } finally { - lock.unlock() } + transactionManager.transaction(readOnly = true) { updateFieldsForGoogle(user) } + val savedUser = transactionManager.transaction(readOnly = false) { saveUserWithConflictRecovery(user) } + adminMenuService.invalidateUser(savedUser.internalId) + return savedUser } private fun updateFieldsForGoogle(user: UserEntity) { @@ -394,45 +419,40 @@ class LoginService( } fun fetchKeycloakUserEntity(profile: KeycloakUserInfoResponse): UserEntity { - val lock = userLocks.lockForKey(profile.sid) - try { - var user: UserEntity - val existingByInternalId = users.findByInternalId(profile.sid) - if (existingByInternalId.isPresent) { - user = existingByInternalId.get() - log.info("Logging in with existing user ${user.fullName} as keycloak user") + var user: UserEntity + val existingByInternalId = users.findByInternalId(profile.sid) + if (existingByInternalId.isPresent) { + user = existingByInternalId.get() + log.info("Logging in with existing user ${user.fullName} as keycloak user") + } else { + val existingByEmail = + if (!profile.email.isBlank()) users.findByEmailIgnoreCase(profile.email) else Optional.empty() + if (existingByEmail.isPresent) { + user = existingByEmail.get() + log.info("Logging in with existing user ${user.fullName} (found by email) as keycloak user, internalId updated from ${user.internalId} to ${profile.sid}") + user.internalId = profile.sid } else { - val existingByEmail = - if (!profile.email.isBlank()) users.findByEmailIgnoreCase(profile.email) else Optional.empty() - if (existingByEmail.isPresent) { - user = existingByEmail.get() - log.info("Logging in with existing user ${user.fullName} (found by email) as keycloak user, internalId updated from ${user.internalId} to ${profile.sid}") - user.internalId = profile.sid - } else { - user = UserEntity( - 0, - profile.sid, - "N/A", - "", - "${profile.familyName} ${profile.givenName}", - profile.preferredUsername, - profile.email, - RoleType.BASIC, - groupName = "", group = null, - guild = GuildType.UNKNOWN, major = MajorType.UNKNOWN, - provider = KEYCLOAK, - profilePicture = "" - ) - log.info("Logging in with new user ${user.fullName} internalId: ${user.internalId} as keycloak user") - } + user = UserEntity( + 0, + profile.sid, + "N/A", + "", + "${profile.familyName} ${profile.givenName}", + profile.preferredUsername, + profile.email, + RoleType.BASIC, + groupName = "", group = null, + guild = GuildType.UNKNOWN, major = MajorType.UNKNOWN, + provider = KEYCLOAK, + profilePicture = "" + ) + log.info("Logging in with new user ${user.fullName} internalId: ${user.internalId} as keycloak user") } - updateFieldsForKeycloak(profile, user) - users.save(user) - adminMenuService.invalidateUser(user.internalId) - return user - } finally { - lock.unlock() } + updateFieldsForKeycloak(profile, user) + val savedUser = saveUserWithConflictRecovery(user) + adminMenuService.invalidateUser(savedUser.internalId) + return savedUser } private fun updateFieldsForKeycloak(profile: KeycloakUserInfoResponse, user: UserEntity) { @@ -483,4 +503,4 @@ class LoginService( user.groupName = user.group?.name ?: "" } -} +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/MessagingTokenEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/MessagingTokenEntity.kt index b4a4c5e1d..d8613e5d8 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/MessagingTokenEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/MessagingTokenEntity.kt @@ -1,6 +1,7 @@ package hu.bme.sch.cmsch.component.pushnotification import jakarta.persistence.* +import org.hibernate.annotations.ColumnDefault import org.springframework.boot.autoconfigure.condition.ConditionalOnBean @Entity @@ -19,5 +20,13 @@ open class MessagingTokenEntity( open var userId: Int = 0, @Column(name = "token", nullable = false) - open var token: String = "" -) + open var token: String = "", + + @ColumnDefault("0") + @Column(name = "createdAt", nullable = false) + open var createdAt: Long = 0, + + @ColumnDefault("0") + @Column(name = "updatedAt", nullable = false) + open var updatedAt: Long = 0 +) \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/MessagingTokenRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/MessagingTokenRepository.kt index 7814d10ce..d96763ac7 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/MessagingTokenRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/MessagingTokenRepository.kt @@ -4,6 +4,7 @@ import hu.bme.sch.cmsch.model.RoleType import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.CrudRepository +import java.util.* @ConditionalOnBean(PushNotificationComponent::class) interface MessagingTokenRepository : CrudRepository { @@ -25,4 +26,10 @@ interface MessagingTokenRepository : CrudRepository fun existsByUserIdAndToken(userId: Int, token: String): Boolean fun deleteByTokenIn(tokens: List): Long + + fun findByUserIdAndToken(userId: Int, token: String): Optional + + fun deleteByUpdatedAtBefore(timestamp: Long): Long + + fun findByUpdatedAtBefore(timestamp: Long): List } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/PushNotificationComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/PushNotificationComponent.kt index 6e69a3a30..503acc475 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/PushNotificationComponent.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/PushNotificationComponent.kt @@ -47,4 +47,9 @@ class PushNotificationComponent( var permissionAllowNeverShowAgain by BooleanSettingRef(true, fieldName = "Tiltás megjegyzése", description = "Ha a felhasználó letiltotta az értesítéseket, akkor többet nem nem kérdez rá az alkalmazás") + val tokenManagementGroup by SettingGroup(fieldName = "Token kezelés") + + var tokenStaleDays by NumberSettingRef(30, fieldName = "Token lejárati idő (nap)", + description = "Ennyi nap inaktivitás után törlődnek a régi FCM tokenek (FCM ajánlás: 30 nap)") + } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/PushNotificationService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/PushNotificationService.kt index eaa72050d..592e3346a 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/PushNotificationService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/pushnotification/PushNotificationService.kt @@ -3,6 +3,7 @@ package hu.bme.sch.cmsch.component.pushnotification import com.google.auth.oauth2.GoogleCredentials import hu.bme.sch.cmsch.dto.CmschNotification import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.service.TimeService import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -10,11 +11,14 @@ import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.retry.RetryTemplate +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Transactional import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.awaitBodilessEntity +import org.springframework.web.reactive.function.client.WebClientResponseException @Service @ConditionalOnBean(PushNotificationComponent::class) @@ -23,6 +27,8 @@ class PushNotificationService( private val projectId: String, private val fcmWebClient: WebClient, private val messagingTokenRepository: MessagingTokenRepository, + private val pushNotificationComponent: PushNotificationComponent, + private val clock: TimeService, private val retryTemplate: RetryTemplate ) { private val log = LoggerFactory.getLogger(javaClass) @@ -60,6 +66,8 @@ class PushNotificationService( return credentials.accessToken.tokenValue } + private data class SendResult(val success: Boolean, val token: String, val invalidToken: Boolean = false) + private fun sendNotifications(tokens: List, notification: CmschNotification): Int = runBlocking { if (tokens.isEmpty()) return@runBlocking 0 @@ -79,27 +87,72 @@ class PushNotificationService( .bodyValue(notification.toFcmRequest(token)) .retrieve() .awaitBodilessEntity() - true + SendResult(success = true, token = token) }.getOrElse { e -> - log.warn("Failed to send notification to token {}: {}", token, e.message) - false + val invalidToken = e is WebClientResponseException.NotFound || + (e is WebClientResponseException.BadRequest && e.responseBodyAsString.contains("UNREGISTERED", ignoreCase = true)) + val redactedToken = token.takeLast(6) + if (invalidToken) { + log.warn("Invalid token detected, will be removed: ...{}", redactedToken) + } else { + log.warn("Failed to send notification to token ...{}: {}", redactedToken, e.message) + } + SendResult(success = false, token = token, invalidToken = invalidToken) } } }.awaitAll() } } - val successCount = results.count { it } + val successCount = results.count { it.success } + val invalidTokens = results.filter { it.invalidToken }.map { it.token } + + if (invalidTokens.isNotEmpty()) { + log.info("Removing {} invalid tokens", invalidTokens.size) + removeTokens(invalidTokens) + } + log.info("Successfully sent notification to {} out of {} devices", successCount, tokens.size) successCount } + private fun removeTokens(tokens: List) { + try { + messagingTokenRepository.deleteByTokenIn(tokens) + } catch (e: Exception) { + log.error("Failed to remove invalid tokens", e) + } + } + @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) fun addToken(userId: Int, token: String) { - if (messagingTokenRepository.existsByUserIdAndToken(userId, token)) return + val now = clock.getTimeInSeconds() + val existing = messagingTokenRepository.findByUserIdAndToken(userId, token) - log.debug("Inserting messaging token for user: {}, token: {}", userId, token) - messagingTokenRepository.save(MessagingTokenEntity(userId = userId, token = token)) + if (existing.isPresent) { + existing.get().updatedAt = now + messagingTokenRepository.save(existing.get()) + return + } + + try { + log.debug("Inserting messaging token for user: {}, token: {}", userId, token) + messagingTokenRepository.save(MessagingTokenEntity( + userId = userId, + token = token, + createdAt = now, + updatedAt = now + )) + } catch (e: DataIntegrityViolationException) { + log.debug("Conflict inserting messaging token for user: {}, updating existing", userId) + val existingAfterConflict = messagingTokenRepository.findByUserIdAndToken(userId, token) + if (existingAfterConflict.isPresent) { + existingAfterConflict.get().updatedAt = now + messagingTokenRepository.save(existingAfterConflict.get()) + } else { + throw e + } + } } @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) @@ -107,4 +160,19 @@ class PushNotificationService( log.debug("Deleting messaging token for user: {}, token: {}", userId, token) messagingTokenRepository.deleteByUserIdAndToken(userId, token) } -} + + @Scheduled(fixedRate = 86400000) + fun cleanupStaleTokens() { + val staleDays = if (pushNotificationComponent.tokenStaleDays < 1) 1 else pushNotificationComponent.tokenStaleDays + val staleThreshold = staleDays.toLong() * 24 * 60 * 60 + val cutoff = clock.getTimeInSeconds() - staleThreshold + try { + val deleted = messagingTokenRepository.deleteByUpdatedAtBefore(cutoff) + if (deleted > 0) { + log.info("Cleaned up {} stale messaging tokens (older than {} days)", deleted, staleDays) + } + } catch (e: Exception) { + log.error("Failed to cleanup stale tokens", e) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/ConcurrentRiddleService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/ConcurrentRiddleService.kt deleted file mode 100644 index cfec6e678..000000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/ConcurrentRiddleService.kt +++ /dev/null @@ -1,112 +0,0 @@ -package hu.bme.sch.cmsch.component.riddle - -import hu.bme.sch.cmsch.component.login.CmschUser -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.stereotype.Service - -@Service -@ConditionalOnBean(RiddleComponent::class) -class ConcurrentRiddleService( - private val cacheManager: RiddleCacheManager, - private val riddleService: RiddleBusinessLogicService -) : RiddleService { - - override fun listRiddlesForUser(user: CmschUser): List { - return riddleService.listRiddlesForUser(user) - } - - override fun listRiddlesForGroup(user: CmschUser, groupId: Int?): List { - return riddleService.listRiddlesForGroup(user, groupId) - } - - override fun getRiddleForUser(user: CmschUser, riddleId: Int): RiddleView? { - return riddleService.getRiddleForUser(user, riddleId) - } - - override fun getRiddleForGroup(user: CmschUser, groupId: Int?, riddleId: Int): RiddleView? { - return riddleService.getRiddleForGroup(user, groupId, riddleId) - } - - override fun unlockHintForUser(user: CmschUser, riddleId: Int): String? { - val lock = cacheManager.getLockForUser(user.id) - lock.lock() - try { - return riddleService.unlockHintForUser(user, riddleId) - } finally { - lock.unlock() - } - } - - override fun unlockHintForGroup(user: CmschUser, groupId: Int?, groupName: String, riddleId: Int): String? { - if (groupId == null) - return null - - val lock = cacheManager.getLockForGroup(groupId) - lock.lock() - try { - return riddleService.unlockHintForGroup(user, groupId, groupName, riddleId) - } finally { - lock.unlock() - } - } - - override fun submitRiddleForUser( - user: CmschUser, - riddleId: Int, - solution: String, - skip: Boolean - ): RiddleSubmissionView? { - val lock = cacheManager.getLockForUser(user.id) - lock.lock() - try { - return riddleService.submitRiddleForUser(user, riddleId, solution, skip) - } finally { - lock.unlock() - } - } - - override fun submitRiddleForGroup( - user: CmschUser, - groupId: Int?, - groupName: String, - riddleId: Int, - solution: String, - skip: Boolean - ): RiddleSubmissionView? { - if (groupId == null) - return null - - val lock = cacheManager.getLockForGroup(groupId) - lock.lock() - try { - return riddleService.submitRiddleForGroup(user, groupId, groupName, riddleId, solution, skip) - } finally { - lock.unlock() - } - } - - override fun getCompletedRiddleCountUser(user: CmschUser): Int { - return riddleService.getCompletedRiddleCountUser(user) - } - - override fun getCompletedRiddleCountGroup(user: CmschUser, groupId: Int?): Int { - return riddleService.getCompletedRiddleCountGroup(user, groupId) - } - - override fun getTotalRiddleCount(user: CmschUser): Int { - return riddleService.getTotalRiddleCount(user) - } - - override fun listRiddleHistoryForUser(user: CmschUser): Map> { - return riddleService.listRiddleHistoryForUser(user) - } - - override fun listRiddleHistoryForGroup( - user: CmschUser, - groupId: Int? - ): Map> { - return riddleService.listRiddleHistoryForGroup(user, groupId) - } - - -} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt index d04431c0c..20deb74de 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt @@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.* @RequestMapping("/api") @ConditionalOnBean(RiddleComponent::class) class RiddleApiController( - private val riddleService: ConcurrentRiddleService, + private val riddleService: RiddleBusinessLogicService, private val startupPropertyConfig: StartupPropertyConfig, private val riddleComponent: RiddleComponent ) { diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt index f8b72e3db..d23227b48 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt @@ -7,14 +7,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.resilience.annotation.Retryable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional -import java.sql.SQLException +import org.springframework.dao.DataAccessException @Service @ConditionalOnBean(RiddleComponent::class) class RiddleBusinessLogicService( - private val riddleCacheManager: RiddleCacheManager, + private val riddleEntityRepository: RiddleEntityRepository, + private val riddleCategoryRepository: RiddleCategoryRepository, + private val riddleMappingRepository: RiddleMappingRepository, private val userService: UserService, private val clock: TimeService, private val riddleComponent: RiddleComponent, @@ -23,10 +24,14 @@ class RiddleBusinessLogicService( @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) override fun listRiddlesForUser(user: CmschUser): List { - val categories = riddleCacheManager.findAllCategoriesByVisibleTrueAndMinRoleAtMost(user.role) - val submissions = riddleCacheManager.findAllMappingByOwnerUserIdAndCompletedTrue(user.id) - .groupBy { it.riddleCategoryId } - .toMap() + val categories = riddleCategoryRepository.findAll() + .filter { it.visible && it.minRole.value <= user.role.value } + val submissionsList = riddleMappingRepository.findAllByOwnerUserId(user.id) + val riddleIds = submissionsList.map { it.riddleId }.toSet() + val riddlesById = riddleEntityRepository.findAllById(riddleIds).associateBy { it.id } + val submissions = submissionsList + .groupBy { riddlesById[it.riddleId]?.categoryId ?: 0 } + .filterKeys { it != 0 } return mapRiddles(categories, submissions) } @@ -36,10 +41,14 @@ class RiddleBusinessLogicService( if (groupId == null) return listOf() - val categories = riddleCacheManager.findAllCategoriesByVisibleTrueAndMinRoleAtMost(user.role) - val submissions = riddleCacheManager.findAllMappingByOwnerGroupIdAndCompletedTrue(groupId) - .groupBy { it.riddleCategoryId } - .toMap() + val categories = riddleCategoryRepository.findAll() + .filter { it.visible && it.minRole.value <= user.role.value } + val submissionsList = riddleMappingRepository.findAllByOwnerGroupId(groupId) + val riddleIds = submissionsList.map { it.riddleId }.toSet() + val riddlesById = riddleEntityRepository.findAllById(riddleIds).associateBy { it.id } + val submissions = submissionsList + .groupBy { riddlesById[it.riddleId]?.categoryId ?: 0 } + .filterKeys { it != 0 } return mapRiddles(categories, submissions) } @@ -47,27 +56,39 @@ class RiddleBusinessLogicService( private fun mapRiddles( categories: List, submissions: Map> - ) = categories.map { category -> - val riddles = riddleCacheManager.findAllRiddleByCategoryId(category.categoryId) - val total = riddles.size - val nextRiddleIds = findNextTo(category.categoryId, submissions[category.categoryId] ?: listOf()) - - RiddleCategoryDto( - categoryId = category.categoryId, - title = category.title, - nextRiddles = nextRiddleIds, - completed = submissions[category.categoryId]?.size ?: 0, - total = total - ) + ): List { + val allRiddles = riddleEntityRepository.findAll().toList() + val riddlesByCategory = allRiddles.groupBy { it.categoryId } + + return categories.map { category -> + val riddles = riddlesByCategory[category.categoryId] ?: emptyList() + val total = riddles.size + val nextRiddleIds = findNextTo(category.categoryId, submissions[category.categoryId] ?: listOf(), allRiddles) + + RiddleCategoryDto( + categoryId = category.categoryId, + title = category.title, + nextRiddles = nextRiddleIds, + completed = submissions[category.categoryId]?.size ?: 0, + total = total + ) + } } @Transactional(readOnly = true) override fun getRiddleForUser(user: CmschUser, riddleId: Int): RiddleView? { - val riddle = riddleCacheManager.getRiddleById(riddleId) ?: return null - riddleCacheManager.findCategoryByCategoryIdAndVisibleTrueAndMinRoleAtMost(riddle.categoryId, user.role) + val riddle = riddleEntityRepository.findById(riddleId).orElse(null) ?: return null + val allCategories = riddleCategoryRepository.findAll().toList() + val category = allCategories + .firstOrNull { it.categoryId == riddle.categoryId && it.visible && it.minRole.value <= user.role.value } ?: return null - val submissions = riddleCacheManager.findAllMappingByOwnerUserIdAndRiddleCategoryId(user.id, riddle.categoryId) + val allRiddles = riddleEntityRepository.findAll().toList() + val riddleIdsInCategory = allRiddles + .filter { it.categoryId == riddle.categoryId } + .map { it.id } + val submissions = riddleMappingRepository.findAllByOwnerUserId(user.id) + .filter { it.riddleId in riddleIdsInCategory } return getRiddleIfAllowedToRead(riddle, submissions) } @@ -76,11 +97,18 @@ class RiddleBusinessLogicService( if (groupId == null) return null - val riddle = riddleCacheManager.getRiddleById(riddleId) ?: return null - riddleCacheManager.findCategoryByCategoryIdAndVisibleTrueAndMinRoleAtMost(riddle.categoryId, user.role) + val riddle = riddleEntityRepository.findById(riddleId).orElse(null) ?: return null + val allCategories = riddleCategoryRepository.findAll().toList() + val category = allCategories + .firstOrNull { it.categoryId == riddle.categoryId && it.visible && it.minRole.value <= user.role.value } ?: return null - val submissions = riddleCacheManager.findAllMappingByGroupUserIdAndRiddleCategoryId(groupId, riddle.categoryId) + val allRiddles = riddleEntityRepository.findAll().toList() + val riddleIdsInCategory = allRiddles + .filter { it.categoryId == riddle.categoryId } + .map { it.id } + val submissions = riddleMappingRepository.findAllByOwnerGroupId(groupId) + .filter { it.riddleId in riddleIdsInCategory } return getRiddleIfAllowedToRead(riddle, submissions) } @@ -127,24 +155,27 @@ class RiddleBusinessLogicService( ) } - @Retryable(value = [SQLException::class], maxRetries = 5, delay = 500L, multiplier = 1.5) + @Retryable(value = [DataAccessException::class], maxRetries = 5, delay = 500L, multiplier = 1.5) @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) override fun unlockHintForUser(user: CmschUser, riddleId: Int): String? { - val riddle = riddleCacheManager.getRiddleById(riddleId) ?: return null - riddleCacheManager.findCategoryByCategoryIdAndVisibleTrueAndMinRoleAtMost(riddle.categoryId, user.role) + val riddle = riddleEntityRepository.findById(riddleId).orElse(null) ?: return null + val allCategories = riddleCategoryRepository.findAll().toList() + allCategories + .firstOrNull { it.categoryId == riddle.categoryId && it.visible && it.minRole.value <= user.role.value } ?: return null - val submission = riddleCacheManager.findMappingByOwnerUserIdAndRiddleId(user.id, riddleId) + val submission = riddleMappingRepository.findAllByOwnerUserId(user.id) + .firstOrNull { it.riddleId == riddleId } if (submission != null) { submission.hintUsed = true - riddleCacheManager.updateMapping(submission) + riddleMappingRepository.save(submission) } else { val nextRiddles = getNextRiddlesUser(user, riddle) if (nextRiddles.find { it.id == riddle.id } == null) return null val userEntity = userService.getById(user.internalId) - riddleCacheManager.createNewMapping( + riddleMappingRepository.save( RiddleMappingEntity(0, riddle.id, userEntity.id, 0, hintUsed = true, completed = false, skipped = false, attemptCount = 0) ) @@ -152,26 +183,29 @@ class RiddleBusinessLogicService( return riddle.hint } - @Retryable(value = [SQLException::class], maxRetries = 5, delay = 500L, multiplier = 1.5) + @Retryable(value = [DataAccessException::class], maxRetries = 5, delay = 500L, multiplier = 1.5) @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) override fun unlockHintForGroup(user: CmschUser, groupId: Int?, groupName: String, riddleId: Int): String? { if (groupId == null) return null - val riddle = riddleCacheManager.getRiddleById(riddleId) ?: return null - riddleCacheManager.findCategoryByCategoryIdAndVisibleTrueAndMinRoleAtMost(riddle.categoryId, user.role) + val riddle = riddleEntityRepository.findById(riddleId).orElse(null) ?: return null + val allCategories = riddleCategoryRepository.findAll().toList() + allCategories + .firstOrNull { it.categoryId == riddle.categoryId && it.visible && it.minRole.value <= user.role.value } ?: return null - val submission = riddleCacheManager.findMappingByOwnerGroupIdAndRiddleId(groupId, riddleId) + val submission = riddleMappingRepository.findAllByOwnerGroupId(groupId) + .firstOrNull { it.riddleId == riddleId } if (submission != null) { submission.hintUsed = true - riddleCacheManager.updateMapping(submission) + riddleMappingRepository.save(submission) } else { val nextRiddles = getNextRiddlesGroup(groupId, riddle) if (nextRiddles.find { it.id == riddle.id } == null) return null - riddleCacheManager.createNewMapping( + riddleMappingRepository.save( RiddleMappingEntity(0, riddle.id, 0, groupId, hintUsed = true, completed = false, skipped = false, attemptCount = 0) ) @@ -179,8 +213,8 @@ class RiddleBusinessLogicService( return riddle.hint } - @Retryable(value = [SQLException::class], maxRetries = 5, delay = 500L, multiplier = 1.5) - @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW) + @Retryable(value = [DataAccessException::class], maxRetries = 5, delay = 500L, multiplier = 1.5) + @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) override fun submitRiddleForUser( user: CmschUser, riddleId: Int, @@ -191,8 +225,10 @@ class RiddleBusinessLogicService( if (banStatus == SubmissionModerationStatus.HARD_BAN) { return RiddleSubmissionView(status = RiddleSubmissionStatus.SUBMITTER_BANNED) } - val riddle = riddleCacheManager.getRiddleById(riddleId) ?: return null - riddleCacheManager.findCategoryByCategoryIdAndVisibleTrueAndMinRoleAtMost(riddle.categoryId, user.role) + val riddle = riddleEntityRepository.findById(riddleId).orElse(null) ?: return null + val allCategories = riddleCategoryRepository.findAll().toList() + allCategories + .firstOrNull { it.categoryId == riddle.categoryId && it.visible && it.minRole.value <= user.role.value } ?: return null if (skip && (cannotSkip(riddle) || !riddleComponent.skipEnabled)) @@ -201,13 +237,14 @@ class RiddleBusinessLogicService( if (banStatus == SubmissionModerationStatus.SHADOW_BAN) { return RiddleSubmissionView(status = RiddleSubmissionStatus.WRONG) } - val submission = riddleCacheManager.findMappingByOwnerUserIdAndRiddleId(user.id, riddleId) + val submission = riddleMappingRepository.findAllByOwnerUserId(user.id) + .firstOrNull { it.riddleId == riddleId } if (submission != null) { if (!skip && checkSolutionIsWrong(solution, riddle)) { if (riddleComponent.saveFailedAttempts) { submission.attemptCount += 1 submission.completedAt = clock.getTimeInSeconds() - riddleCacheManager.updateMapping(submission, lazyPersist = true) + riddleMappingRepository.save(submission) } return RiddleSubmissionView(status = RiddleSubmissionStatus.WRONG) } @@ -217,10 +254,10 @@ class RiddleBusinessLogicService( submission.skipped = skip submission.attemptCount += 1 submission.completedAt = clock.getTimeInSeconds() - riddleCacheManager.updateMapping(submission) + riddleMappingRepository.save(submission) if (!skip && riddle.firstSolver.isBlank()) { riddle.firstSolver = user.userName - riddleCacheManager.updateRiddle(riddle) + riddleEntityRepository.save(riddle) } } return RiddleSubmissionView(status = RiddleSubmissionStatus.CORRECT, getNextRiddlesUser(user, riddle)) @@ -232,7 +269,7 @@ class RiddleBusinessLogicService( val userEntity = userService.getById(user.internalId) if (!skip && checkSolutionIsWrong(solution, riddle)) { if (riddleComponent.saveFailedAttempts) { - riddleCacheManager.createNewMapping( + riddleMappingRepository.save( RiddleMappingEntity( 0, riddle.id, userEntity.id, 0, hintUsed = false, completed = false, skipped = false, @@ -243,21 +280,21 @@ class RiddleBusinessLogicService( return RiddleSubmissionView(status = RiddleSubmissionStatus.WRONG) } - riddleCacheManager.createNewMapping( + riddleMappingRepository.save( RiddleMappingEntity(0, riddle.id, userEntity.id, 0, hintUsed = false, completed = true, skipped = skip, completedAt = clock.getTimeInSeconds(), attemptCount = 1) ) if (!skip && riddle.firstSolver.isBlank()) { riddle.firstSolver = user.userName - riddleCacheManager.updateRiddle(riddle) + riddleEntityRepository.save(riddle) } return RiddleSubmissionView(status = RiddleSubmissionStatus.CORRECT, getNextRiddlesUser(user, riddle)) } } - @Retryable(value = [SQLException::class], maxRetries = 5, delay = 500L, multiplier = 1.5) - @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW) + @Retryable(value = [DataAccessException::class], maxRetries = 5, delay = 500L, multiplier = 1.5) + @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) override fun submitRiddleForGroup( user: CmschUser, groupId: Int?, @@ -274,8 +311,10 @@ class RiddleBusinessLogicService( return RiddleSubmissionView(status = RiddleSubmissionStatus.SUBMITTER_BANNED) } - val riddle = riddleCacheManager.getRiddleById(riddleId) ?: return null - riddleCacheManager.findCategoryByCategoryIdAndVisibleTrueAndMinRoleAtMost(riddle.categoryId, user.role) + val riddle = riddleEntityRepository.findById(riddleId).orElse(null) ?: return null + val allCategories = riddleCategoryRepository.findAll().toList() + allCategories + .firstOrNull { it.categoryId == riddle.categoryId && it.visible && it.minRole.value <= user.role.value } ?: return null if (skip && (cannotSkip(riddle) || !riddleComponent.skipEnabled)) @@ -285,13 +324,14 @@ class RiddleBusinessLogicService( return RiddleSubmissionView(status = RiddleSubmissionStatus.WRONG) } - val submission = riddleCacheManager.findMappingByOwnerGroupIdAndRiddleId(groupId, riddleId) + val submission = riddleMappingRepository.findAllByOwnerGroupId(groupId) + .firstOrNull { it.riddleId == riddleId } if (submission != null) { if (!skip && checkSolutionIsWrong(solution, riddle)) { if (riddleComponent.saveFailedAttempts) { submission.attemptCount += 1 submission.completedAt = clock.getTimeInSeconds() - riddleCacheManager.updateMapping(submission, lazyPersist = true) + riddleMappingRepository.save(submission) } return RiddleSubmissionView(status = RiddleSubmissionStatus.WRONG) } @@ -301,10 +341,10 @@ class RiddleBusinessLogicService( submission.skipped = skip submission.attemptCount += 1 submission.completedAt = clock.getTimeInSeconds() - riddleCacheManager.updateMapping(submission) + riddleMappingRepository.save(submission) if (!skip && riddle.firstSolver.isBlank()) { riddle.firstSolver = groupName - riddleCacheManager.updateRiddle(riddle) + riddleEntityRepository.save(riddle) } } return RiddleSubmissionView(status = RiddleSubmissionStatus.CORRECT, getNextRiddlesGroup(groupId, riddle)) @@ -315,7 +355,7 @@ class RiddleBusinessLogicService( return null if (!skip && checkSolutionIsWrong(solution, riddle)) { if (riddleComponent.saveFailedAttempts) { - riddleCacheManager.createNewMapping( + riddleMappingRepository.save( RiddleMappingEntity( 0, riddle.id, 0, groupId, hintUsed = false, completed = false, skipped = false, @@ -326,7 +366,7 @@ class RiddleBusinessLogicService( return RiddleSubmissionView(status = RiddleSubmissionStatus.WRONG) } - riddleCacheManager.createNewMapping( + riddleMappingRepository.save( RiddleMappingEntity(0, riddle.id, 0, groupId, hintUsed = false, completed = true, skipped = skip, completedAt = clock.getTimeInSeconds(), attemptCount = 1 @@ -334,14 +374,14 @@ class RiddleBusinessLogicService( ) if (!skip && riddle.firstSolver.isBlank()) { riddle.firstSolver = groupName - riddleCacheManager.updateRiddle(riddle) + riddleEntityRepository.save(riddle) } return RiddleSubmissionView(status = RiddleSubmissionStatus.CORRECT, getNextRiddlesGroup(groupId, riddle)) } } private fun cannotSkip(riddle: RiddleEntity): Boolean { - return riddleCacheManager.countAllMappingByCompletedNotSkippedAndRiddleId(riddle.id) < + return riddleMappingRepository.countAllByCompletedTrueAndSkippedFalseAndRiddleId(riddle.id) < riddleComponent.skipAfterGroupsSolved } @@ -398,57 +438,99 @@ class RiddleBusinessLogicService( .replace("ű", "u") private fun getNextRiddlesUser(user: CmschUser, riddle: RiddleEntity): List { - val submissions = riddleCacheManager.findAllMappingByOwnerUserIdAndRiddleCategoryId(user.id, riddle.categoryId) - return findNextTo(riddle.categoryId, submissions) + val allRiddles = riddleEntityRepository.findAll().toList() + val riddleIdsInCategory = allRiddles + .filter { it.categoryId == riddle.categoryId } + .map { it.id } + val submissions = riddleMappingRepository.findAllByOwnerUserId(user.id) + .filter { it.riddleId in riddleIdsInCategory } + return findNextTo(riddle.categoryId, submissions, allRiddles) } private fun getNextRiddlesGroup(groupId: Int, riddle: RiddleEntity): List { - val submissions = riddleCacheManager.findAllMappingByGroupUserIdAndRiddleCategoryId(groupId, riddle.categoryId) - return findNextTo(riddle.categoryId, submissions) + val allRiddles = riddleEntityRepository.findAll().toList() + val riddleIdsInCategory = allRiddles + .filter { it.categoryId == riddle.categoryId } + .map { it.id } + val submissions = riddleMappingRepository.findAllByOwnerGroupId(groupId) + .filter { it.riddleId in riddleIdsInCategory } + return findNextTo(riddle.categoryId, submissions, allRiddles) } private fun findNextTo( categoryId: Int, - submissions: List + submissions: List, + allRiddles: List ): List = - riddleCacheManager.findAllRiddleByCategoryId(categoryId) + allRiddles + .filter { it.categoryId == categoryId } .filter { filter -> submissions.none { it.completed && it.riddleId == filter.id } } .sortedBy { it.order } .take(riddleComponent.visibleRiddlesPerCategory.toInt()) .map { mapRiddleView(submissions, it) } + private fun findNextTo( + categoryId: Int, + submissions: List + ): List { + val allRiddles = riddleEntityRepository.findAll().toList() + return findNextTo(categoryId, submissions, allRiddles) + } + @Transactional(readOnly = true) override fun getCompletedRiddleCountUser(user: CmschUser): Int { - val categories = riddleCacheManager.findAllCategoriesByVisibleTrueAndMinRoleAtMost(user.role).map { it.categoryId } - return riddleCacheManager.countAllMappingByCompletedTrueAndOwnerUserIdAndRiddleCategoryIdIn(user.id, categories) + val allCategories = riddleCategoryRepository.findAll().toList() + val categoryIds = allCategories + .filter { it.visible && it.minRole.value <= user.role.value } + .map { it.categoryId } + val allRiddles = riddleEntityRepository.findAll().toList() + val riddleIds = allRiddles + .filter { it.categoryId in categoryIds } + .map { it.id } + return riddleMappingRepository.countAllByCompletedTrueAndOwnerUserIdAndRiddleIdIn(user.id, riddleIds) } @Transactional(readOnly = true) override fun getCompletedRiddleCountGroup(user: CmschUser, groupId: Int?): Int { if (groupId == null) return 0 - val categories = riddleCacheManager.findAllCategoriesByVisibleTrueAndMinRoleAtMost(user.role).map { it.categoryId } - return riddleCacheManager.countAllMappingByCompletedTrueAndOwnerGroupIdAndRiddleCategoryIdIn(groupId, categories) + val allCategories = riddleCategoryRepository.findAll().toList() + val categoryIds = allCategories + .filter { it.visible && it.minRole.value <= user.role.value } + .map { it.categoryId } + val allRiddles = riddleEntityRepository.findAll().toList() + val riddleIds = allRiddles + .filter { it.categoryId in categoryIds } + .map { it.id } + return riddleMappingRepository.countAllByCompletedTrueAndOwnerGroupIdAndRiddleIdIn(groupId, riddleIds) } @Transactional(readOnly = true) override fun getTotalRiddleCount(user: CmschUser): Int { - val categories = riddleCacheManager.findAllCategoriesByVisibleTrueAndMinRoleAtMost(user.role) + val allCategories = riddleCategoryRepository.findAll().toList() + val categoryIds = allCategories + .filter { it.visible && it.minRole.value <= user.role.value } .map { it.categoryId } - return riddleCacheManager.countAllRiddleByCategoryIdIn(categories) + val allRiddles = riddleEntityRepository.findAll().toList() + return allRiddles + .count { it.categoryId in categoryIds } } @Transactional(readOnly = true) override fun listRiddleHistoryForUser(user: CmschUser): Map> { - val categories = riddleCacheManager.findAllCategoriesByVisibleTrueAndMinRoleAtMost(user.role) - val submissions = riddleCacheManager.findAllMappingByOwnerUserIdAndCompletedTrue(user.id) - .groupBy { riddleCacheManager.getRiddleById(it.riddleId)?.categoryId ?: 0 } - .toMap() + val categories = riddleCategoryRepository.findAll().toList() + .filter { it.visible && it.minRole.value <= user.role.value } + val submissionsList = riddleMappingRepository.findAllByOwnerUserIdAndCompletedTrue(user.id) + val riddleIds = submissionsList.map { it.riddleId }.toSet() + val riddlesById = riddleEntityRepository.findAllById(riddleIds).associateBy { it.id } + val submissions = submissionsList + .groupBy { riddlesById[it.riddleId]?.categoryId ?: 0 } + .filterKeys { it != 0 } return categories.associate { category -> category.title to submissions.getOrDefault(category.categoryId, listOf()) - .map { riddle -> riddle to riddleCacheManager.getRiddleById(riddle.riddleId) } + .map { mapping -> mapping to riddlesById[mapping.riddleId] } .sortedBy { it.second?.order } .mapNotNull { it.second?.let { riddle -> mapRiddle(it.first, riddle) } } .toList() @@ -460,14 +542,18 @@ class RiddleBusinessLogicService( if (groupId == null) return mapOf() - val categories = riddleCacheManager.findAllCategoriesByVisibleTrueAndMinRoleAtMost(user.role) - val submissions = riddleCacheManager.findAllMappingByOwnerGroupIdAndCompletedTrue(groupId) - .groupBy { riddleCacheManager.getRiddleById(it.riddleId)?.categoryId ?: 0 } - .toMap() + val categories = riddleCategoryRepository.findAll().toList() + .filter { it.visible && it.minRole.value <= user.role.value } + val submissionsList = riddleMappingRepository.findAllByOwnerGroupIdAndCompletedTrue(groupId) + val riddleIds = submissionsList.map { it.riddleId }.toSet() + val riddlesById = riddleEntityRepository.findAllById(riddleIds).associateBy { it.id } + val submissions = submissionsList + .groupBy { riddlesById[it.riddleId]?.categoryId ?: 0 } + .filterKeys { it != 0 } return categories.associate { category -> category.title to submissions.getOrDefault(category.categoryId, listOf()) - .map { riddle -> riddle to riddleCacheManager.getRiddleById(riddle.riddleId) } + .map { mapping -> mapping to riddlesById[mapping.riddleId] } .sortedBy { it.second?.order } .mapNotNull { it.second?.let { riddle -> mapRiddle(it.first, riddle) } } .toList() @@ -488,4 +574,4 @@ class RiddleBusinessLogicService( ) } -} +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleCacheManager.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleCacheManager.kt deleted file mode 100644 index a50a01833..000000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleCacheManager.kt +++ /dev/null @@ -1,200 +0,0 @@ -package hu.bme.sch.cmsch.component.riddle - -import hu.bme.sch.cmsch.config.StartupPropertyConfig -import hu.bme.sch.cmsch.model.RoleType -import jakarta.annotation.PostConstruct -import org.slf4j.LoggerFactory -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.resilience.annotation.Retryable -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Propagation -import org.springframework.transaction.annotation.Transactional -import java.sql.SQLException -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.locks.ReentrantLock - - -/** - * This service is only designed to work for the frontend direction. - * Admin page actions still requires using the Repositories. - */ -@Service -@ConditionalOnBean(RiddleComponent::class) -class RiddleCacheManager( - private val riddleEntityRepository: RiddleEntityRepository, - private val riddleCategoryRepository: RiddleCategoryRepository, - private val riddleMappingRepository: RiddleMappingRepository, - private val riddlePersistenceService: RiddlePersistenceService, - private val config: StartupPropertyConfig -) { - - private val log = LoggerFactory.getLogger(javaClass) - - private val riddles = ConcurrentHashMap() - private val categories = ConcurrentHashMap() - private val mappings = ConcurrentHashMap() - private val lazyPersists = Collections.synchronizedSet(HashSet()) - - private val groupLocks = ConcurrentHashMap() - private val userLocks = ConcurrentHashMap() - - fun getLockForGroup(groupId: Int): ReentrantLock { - return groupLocks.computeIfAbsent(groupId) { ReentrantLock() } - } - - fun getLockForUser(userId: Int): ReentrantLock { - return userLocks.computeIfAbsent(userId) { ReentrantLock() } - } - - @PostConstruct - fun init() { - if (config.masterRole && config.riddleMicroserviceEnabled) { - log.info("Riddle periodic save is disabled") - return - } - resetCache(persistMapping = false, overrideMappings = true) - } - - @Retryable(value = [ SQLException::class ], maxRetries = 5, delay = 500L, multiplier = 1.5) - @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW) - fun resetCache(persistMapping: Boolean, overrideMappings: Boolean) { - log.info("Getting all locks for 'resetCache({}, {})'", persistMapping, overrideMappings) - groupLocks.forEach { (_, lock) -> lock.lock() } - userLocks.forEach { (_, lock) -> lock.lock() } - log.info("Got all locks for 'resetCache({}, {})'", persistMapping, overrideMappings) - try { - riddles.clear() - categories.clear() - if (persistMapping) { - riddleMappingRepository.saveAll(mappings.values) - } - if (overrideMappings) { - mappings.clear() - } - - riddles.putAll(riddleEntityRepository.findAll().associateBy { it.id }) - categories.putAll(riddleCategoryRepository.findAll().associateBy { it.id }) - if (overrideMappings) { - mappings.putAll(riddleMappingRepository.findAll().associateBy { it.id }) - } - mappings.forEach { - it.value.riddleCategoryId = riddles[it.value.riddleId]?.categoryId ?: 0 - } - } finally { - groupLocks.forEach { (_, lock) -> lock.unlock() } - userLocks.forEach { (_, lock) -> lock.unlock() } - log.info("All locks released by 'resetCache({}, {})'", persistMapping, overrideMappings) - } - } - - fun forceUnlock() { - groupLocks.forEach { (_, lock) -> lock.unlock() } - userLocks.forEach { (_, lock) -> lock.unlock() } - log.info("All locks released by 'forceUnlock'") - } - - @Scheduled(fixedRate = 5 * 60 * 1000) - fun periodicSave() { - if (config.masterRole && config.riddleMicroserviceEnabled) { - log.info("Riddle periodic save is disabled") - return - } - - log.info("Getting all locks for 'periodicSave'") - groupLocks.forEach { (_, lock) -> lock.lock() } - userLocks.forEach { (_, lock) -> lock.lock() } - log.info("Got all locks for 'periodicSave'") - try { - riddlePersistenceService.saveAllRiddleMapping(lazyPersists) - lazyPersists.clear() - } finally { - groupLocks.forEach { (_, lock) -> lock.unlock() } - userLocks.forEach { (_, lock) -> lock.unlock() } - log.info("All locks released by 'periodicSave'") - groupLocks.clear() - userLocks.clear() - } - } - - fun createNewMapping(mapping: RiddleMappingEntity) { - riddlePersistenceService.saveRiddleMapping(mapping) - mappings[mapping.id] = mapping - mapping.riddleCategoryId = riddles[mapping.riddleId]?.categoryId ?: 0 - } - - fun updateMapping(mapping: RiddleMappingEntity, lazyPersist: Boolean = false) { - if (lazyPersist) { - lazyPersists.add(mapping) - } else { - riddlePersistenceService.saveRiddleMapping(mapping) - } - } - - fun updateRiddle(riddle: RiddleEntity) { - riddlePersistenceService.saveRiddle(riddle) - } - - fun getRiddleById(riddleId: Int) = riddles[riddleId] - - fun findAllCategoriesByVisibleTrueAndMinRoleAtMost(role: RoleType) = - categories.values.asSequence() - .filter { it.visible && it.minRole.value <= role.value } - .toList() - - fun findAllMappingByOwnerUserIdAndCompletedTrue(userId: Int) = - mappings.values.asSequence() - .filter { it.ownerUserId == userId && it.completed } - .toList() - - fun findAllMappingByOwnerGroupIdAndCompletedTrue(groupId: Int) = - mappings.values.asSequence() - .filter { it.ownerGroupId == groupId && it.completed } - .toList() - - fun findAllRiddleByCategoryId(categoryId: Int) = - riddles.values.asSequence() - .filter { it.categoryId == categoryId } - .toList() - - fun findCategoryByCategoryIdAndVisibleTrueAndMinRoleAtMost(categoryId: Int, role: RoleType) = - categories.values - .firstOrNull { it.visible && it.categoryId == categoryId && it.minRole.value <= role.value } - - fun findAllMappingByOwnerUserIdAndRiddleCategoryId(userId: Int, categoryId: Int) = - mappings.values.asSequence() - .filter { it.ownerUserId == userId && it.riddleCategoryId == categoryId } - .toList() - - fun findAllMappingByGroupUserIdAndRiddleCategoryId(groupId: Int, categoryId: Int) = - mappings.values.asSequence() - .filter { it.ownerGroupId == groupId && it.riddleCategoryId == categoryId } - .toList() - - fun findMappingByOwnerUserIdAndRiddleId(userId: Int, riddleId: Int) = - mappings.values - .firstOrNull { it.ownerUserId == userId && it.riddleId == riddleId } - - fun findMappingByOwnerGroupIdAndRiddleId(groupId: Int, riddleId: Int) = - mappings.values - .firstOrNull { it.ownerGroupId == groupId && it.riddleId == riddleId } - - fun countAllMappingByCompletedNotSkippedAndRiddleId(riddleId: Int) = - mappings.values - .count { it.completed && !it.skipped && it.riddleId == riddleId } - - fun countAllMappingByCompletedTrueAndOwnerUserIdAndRiddleCategoryIdIn(userId: Int, categories: List) = - mappings.values - .count { it.completed && it.ownerUserId == userId && it.riddleCategoryId in categories } - - fun countAllMappingByCompletedTrueAndOwnerGroupIdAndRiddleCategoryIdIn(groupId: Int, categories: List) = - mappings.values - .count { it.completed && it.ownerGroupId == groupId && it.riddleCategoryId in categories } - - fun countAllRiddleByCategoryIdIn(categories: List) = - riddles.values - .count { it.categoryId in categories } - -} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleCategoryController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleCategoryController.kt index abbe4918e..08baf1a96 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleCategoryController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleCategoryController.kt @@ -23,7 +23,6 @@ class RiddleCategoryController( transactionManager: PlatformTransactionManager, env: Environment, storageService: StorageService, - private val riddleCacheManager: RiddleCacheManager ) : OneDeepEntityPage( "riddle-categories", RiddleCategoryEntity::class, ::RiddleCategoryEntity, @@ -55,10 +54,4 @@ class RiddleCategoryController( adminMenuPriority = 2, searchSettings = calculateSearchSettings(false) -) { - - override fun onEntityChanged(entity: RiddleCategoryEntity) { - riddleCacheManager.resetCache(persistMapping = false, overrideMappings = false) - } - -} +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleComponent.kt index 27ee8b020..af725bb06 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleComponent.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleComponent.kt @@ -100,16 +100,6 @@ class RiddleComponent( /// ------------------------------------------------------------------------------------------------------------------- - val microserviceGroup by SettingGroup(fieldName = "Riddle microservice", - description = "A riddle megoldások ellenőrzése kiszervezhető egy külső szolgáltatásba") - - var microserviceNodeBaseUrl by StringSettingRef("http://..svc.cluster.local", - serverSideOnly = true, type = SettingType.URL, fieldName = "Riddle node belső URL-je", - description = "A riddle node elérhetősége a belső hálózaton (pl. Kubernetes clusteren belül)") - - var microserviceSyncEnabled by BooleanSettingRef(false, fieldName = "Beállítások szinkronizálása", - description = "Bekapcsolt állapotban a rendszer értesíti a node-ot a riddle-ök módosításáról a cache invalidálásához (nincs implementálva)") - override fun onPersist() { super.onPersist() updateBanLists() diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleController.kt index f0d25c5b6..a4863ab4a 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleController.kt @@ -23,7 +23,6 @@ class RiddleController( transactionManager: PlatformTransactionManager, env: Environment, storageService: StorageService, - private val riddleCacheManager: RiddleCacheManager ) : OneDeepEntityPage( "riddles", RiddleEntity::class, ::RiddleEntity, @@ -55,14 +54,4 @@ class RiddleController( adminMenuPriority = 1, searchSettings = calculateSearchSettings(false) -) { - - override fun onEntityChanged(entity: RiddleEntity) { - riddleCacheManager.resetCache(persistMapping = false, overrideMappings = false) - } - - override fun onImported() { - riddleCacheManager.resetCache(persistMapping = false, overrideMappings = false) - } - -} +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMappingEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMappingEntity.kt index 71a2682f4..bb29ea381 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMappingEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMappingEntity.kt @@ -58,9 +58,6 @@ data class RiddleMappingEntity( ): ManagedEntity, Duplicatable { - @Transient - var riddleCategoryId: Int = 0 - override fun getEntityConfig(env: Environment) = EntityConfig( name = "RiddleMapping", view = if (env.getProperty("hu.bme.sch.cmsch.startup.riddle-ownership-mode") === "USER") diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMappingRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMappingRepository.kt index 56bb29d56..5cae3ea82 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMappingRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMappingRepository.kt @@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository @Repository @ConditionalOnBean(RiddleComponent::class) -@Suppress("FunctionName", "kotlin:S100") // This is the valid naming conversion of spring-data +@Suppress("FunctionName", "kotlin:S100") interface RiddleMappingRepository : CrudRepository, EntityPageDataSource { @@ -19,5 +19,20 @@ interface RiddleMappingRepository : CrudRepository, fun countAllByOwnerGroupIdAndCompletedTrueAndSkippedTrue(groupId: Int): Int + fun findAllByOwnerUserIdAndCompletedTrue(userId: Int): List + + fun findAllByOwnerGroupIdAndCompletedTrue(groupId: Int): List + + fun findAllByOwnerUserIdAndRiddleId(userId: Int, riddleId: Int): List + + fun findAllByOwnerGroupIdAndRiddleId(groupId: Int, riddleId: Int): List + + fun countAllByCompletedTrueAndRiddleIdIn(riddleIds: List): Int + + fun countAllByCompletedTrueAndOwnerUserIdAndRiddleIdIn(userId: Int, riddleIds: List): Int + + fun countAllByCompletedTrueAndOwnerGroupIdAndRiddleIdIn(groupId: Int, riddleIds: List): Int + + fun countAllByCompletedTrueAndSkippedFalseAndRiddleId(riddleId: Int): Int } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMicroserviceController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMicroserviceController.kt deleted file mode 100644 index fa36e5d3d..000000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMicroserviceController.kt +++ /dev/null @@ -1,97 +0,0 @@ -package hu.bme.sch.cmsch.component.riddle - -import hu.bme.sch.cmsch.config.StartupPropertyConfig -import org.slf4j.LoggerFactory -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.boot.info.BuildProperties -import org.springframework.core.env.Environment -import org.springframework.web.bind.annotation.* - -private const val INVALID_TOKEN = "invalid-token" -private const val OK = "ok" -private const val DISABLED = "ok" - -@RestController -@ConditionalOnBean(RiddleComponent::class) -@RequestMapping("/remote-api/riddle") -@CrossOrigin(originPatterns = ["*"], allowedHeaders = ["*"]) -class RiddleMicroserviceController( - private val riddleComponent: RiddleComponent, - private val riddleCacheManager: RiddleCacheManager, - private val startupPropertyConfig: StartupPropertyConfig, - private val env: Environment, - private val buildProperties: BuildProperties?, -) { - - private val log = LoggerFactory.getLogger(javaClass) - - @PostMapping("/reload-riddle-and-category-cache") - fun reloadRiddleAndCategoryCache(@RequestHeader token: String): String { - if (!startupPropertyConfig.riddleMicroserviceEnabled) { - return DISABLED - } - if (token != startupPropertyConfig.managementToken) { - return INVALID_TOKEN - } - log.info("Calling remote command: reloadRiddleAndCategoryCache") - riddleCacheManager.resetCache(persistMapping = false, overrideMappings = false) - riddleComponent.updateBanLists() - return OK - } - - @PostMapping("/reload-all") - fun reloadAll(@RequestHeader token: String): String { - if (!startupPropertyConfig.riddleMicroserviceEnabled) { - return DISABLED - } - if (token != startupPropertyConfig.managementToken) { - return INVALID_TOKEN - } - log.info("Calling remote command: reloadAll") - riddleCacheManager.resetCache(persistMapping = false, overrideMappings = true) - return OK - } - - @PostMapping("/save-all") - fun saveAll(@RequestHeader token: String): String { - if (!startupPropertyConfig.riddleMicroserviceEnabled) { - return DISABLED - } - if (token != startupPropertyConfig.managementToken) { - return INVALID_TOKEN - } - log.info("Calling remote command: saveAll") - riddleCacheManager.resetCache(persistMapping = true, overrideMappings = true) - return OK - } - - @PostMapping("/force-unlock-everything") - fun forceUnlockEverything(@RequestHeader token: String): String { - if (!startupPropertyConfig.riddleMicroserviceEnabled) { - return DISABLED - } - if (token != startupPropertyConfig.managementToken) { - return INVALID_TOKEN - } - log.info("Calling remote command: forceUnlockEverything") - riddleCacheManager.forceUnlock() - return OK - } - - @PostMapping("/ping") - fun ping(@RequestHeader token: String): String { - if (!startupPropertyConfig.riddleMicroserviceEnabled) { - return DISABLED - } - if (token != startupPropertyConfig.managementToken) { - return INVALID_TOKEN - } - log.info("Calling remote command: ping") - riddleComponent.allSettings.forEach { - log.info("${it.component}.${it.property} = ${it.getValue()}") - } - log.info("cmsch.frontend.production-url = ${env.getProperty("cmsch.frontend.production-url")}") - return "PONG ${startupPropertyConfig.nodeName} ${buildProperties?.version ?: "n/a"}" - } - -} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMicroserviceDashboard.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMicroserviceDashboard.kt deleted file mode 100644 index 2963325d5..000000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleMicroserviceDashboard.kt +++ /dev/null @@ -1,239 +0,0 @@ -package hu.bme.sch.cmsch.component.riddle - -import hu.bme.sch.cmsch.admin.dashboard.DashboardComponent -import hu.bme.sch.cmsch.admin.dashboard.DashboardFormCard -import hu.bme.sch.cmsch.admin.dashboard.DashboardPage -import hu.bme.sch.cmsch.admin.dashboard.DashboardPermissionCard -import hu.bme.sch.cmsch.component.login.CmschUser -import hu.bme.sch.cmsch.config.StartupPropertyConfig -import hu.bme.sch.cmsch.service.AdminMenuService -import hu.bme.sch.cmsch.service.AuditLogService -import hu.bme.sch.cmsch.service.ControlPermissions -import hu.bme.sch.cmsch.util.getUser -import org.slf4j.LoggerFactory -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.http.HttpMethod -import org.springframework.http.MediaType -import org.springframework.security.core.Authentication -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.reactive.function.client.WebClientException - -@Controller -@RequestMapping("/admin/control/riddle-ms") -@ConditionalOnBean(RiddleComponent::class) -class RiddleMicroserviceDashboard( - adminMenuService: AdminMenuService, - applicationComponent: RiddleComponent, - private val auditLogService: AuditLogService, - private val startupPropertyConfig: StartupPropertyConfig, - private val riddleComponent: RiddleComponent -) : DashboardPage( - "riddle-ms", - "Riddle MS dashboard", - "Riddle microservice konfigurációs panel", - false, - adminMenuService, - applicationComponent, - auditLogService, - ControlPermissions.PERMISSION_CONTROL_RIDDLE, - adminMenuCategory = null, - adminMenuIcon = "tune", - adminMenuPriority = 5, - ignoreFromMenu = !startupPropertyConfig.riddleMicroserviceEnabled -) { - - private val log = LoggerFactory.getLogger(javaClass) - - private val permissionCard = DashboardPermissionCard( - 1, - permission = showPermission.permissionString, - description = "Ez a jog szükséges ennek az oldalnak az olvasásához.", - wide = false - ) - - override fun getComponents(user: CmschUser, requestParams: Map): List { - return listOf( - permissionCard, - pingForm(), - reloadRiddleAndCategoryCache(), - reloadAllForm(), - saveAllForm(), - forceUnlockEverythingForm(), - ) - } - - private fun reloadRiddleAndCategoryCache(): DashboardFormCard { - return DashboardFormCard( - 3, - false, - "Riddle és kategória cache újratöltése", - "Riddle és Riddle kategória cache frissítése a riddle nodeon", - listOf(), - buttonCaption = "Frissítés", - buttonIcon = "start", - action = "reload-riddle-and-category-cache", - method = "post" - ) - } - - @PostMapping("/reload-riddle-and-category-cache") - fun reloadRiddleAndCategoryCachePost(auth: Authentication): String { - val user = auth.getUser() - if (!showPermission.validate(user)) { - throw IllegalStateException("Insufficient permissions") - } - - val status = sendRequest("reload-riddle-and-category-cache", user) - - return "redirect:/admin/control/riddle-ms?card=3&message=$status" - } - - private fun reloadAllForm(): DashboardFormCard { - return DashboardFormCard( - 4, - false, - "Teljes cache ürítése", - "A teljes riddle cache frissítése a riddle nodeon. Ettől lehet, hogy adatvesztés fog történni!", - listOf(), - buttonCaption = "Újratöltés", - buttonIcon = "start", - action = "reload-all", - method = "post" - ) - } - - @PostMapping("/reload-all") - fun reloadAllPost(auth: Authentication): String { - val user = auth.getUser() - if (!showPermission.validate(user)) { - throw IllegalStateException("Insufficient permissions") - } - - val status = sendRequest("reload-all", user) - - return "redirect:/admin/control/riddle-ms?card=4&message=$status" - } - - private fun saveAllForm(): DashboardFormCard { - return DashboardFormCard( - 5, - false, - "Minden adat lementése", - "A riddle cache lementése a riddle nodeon", - listOf(), - buttonCaption = "Minden mentése", - buttonIcon = "start", - action = "save-all", - method = "post" - ) - } - - @PostMapping("/save-all") - fun saveAllPost(auth: Authentication): String { - val user = auth.getUser() - if (!showPermission.validate(user)) { - throw IllegalStateException("Insufficient permissions") - } - - val status = sendRequest("save-all", user) - - return "redirect:/admin/control/riddle-ms?card=5&message=$status" - } - - private fun forceUnlockEverythingForm(): DashboardFormCard { - return DashboardFormCard( - 6, - false, - "Komponens config újratöltése", - "Az összes lock felengedése", - listOf(), - buttonCaption = "Felengedés", - buttonIcon = "start", - action = "force-unlock-everything", - method = "post" - ) - } - - @PostMapping("/force-unlock-everything") - fun forceUnlockEverythingPost(auth: Authentication): String { - val user = auth.getUser() - if (!showPermission.validate(user)) { - throw IllegalStateException("Insufficient permissions") - } - - val status = sendRequest("force-unlock-everything", user) - - return "redirect:/admin/control/riddle-ms?card=6&message=$status" - } - - private fun pingForm(): DashboardFormCard { - return DashboardFormCard( - 7, - false, - "Ping", - "Kapcsolat tesztelése", - listOf(), - buttonCaption = "PING", - buttonIcon = "network_ping", - action = "ping", - method = "post" - ) - } - - @PostMapping("/ping") - fun pingPost(auth: Authentication): String { - val user = auth.getUser() - if (!showPermission.validate(user)) { - throw IllegalStateException("Insufficient permissions") - } - - val status = sendRequest("ping", user) - - return "redirect:/admin/control/riddle-ms?card=7&message=$status" - } - - private fun sendRequest(path: String, user: CmschUser?): String { - val client = WebClient.builder() - .baseUrl(riddleComponent.microserviceNodeBaseUrl) - .defaultHeaders { header -> header.add("token", startupPropertyConfig.managementToken) } - .build() - - val request = client.method(HttpMethod.POST) - .uri("${riddleComponent.microserviceNodeBaseUrl}/remote-api/riddle/${path}") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - - return retrieve(request, path, user) - } - - private fun retrieve( - request: WebClient.RequestHeadersSpec<*>, - to: String, - responsible: CmschUser? - ): String { - try { - val response = request.retrieve().toEntity(String::class.java).block()?.body ?: "NO_ANSWER" - - val action = "Remote command sent to:$to response:$response" - log.info(action) - if (responsible != null) { - auditLogService.fine(responsible, "riddle", action) - } else { - auditLogService.system("riddle", action) - } - return response - } catch (e: WebClientException) { - val action = "Remote command failed to send to:$to e:${e.message}" - log.error(action) - if (responsible != null) { - auditLogService.error(responsible, "riddle", action) - } else { - auditLogService.system("riddle", action) - } - } - return "ERROR_SEE_LOG" - } - -} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddlePersistenceService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddlePersistenceService.kt deleted file mode 100644 index 75ba8ad0f..000000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddlePersistenceService.kt +++ /dev/null @@ -1,34 +0,0 @@ -package hu.bme.sch.cmsch.component.riddle - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.resilience.annotation.Retryable -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Propagation -import org.springframework.transaction.annotation.Transactional -import java.sql.SQLException - -@Service -@ConditionalOnBean(RiddleComponent::class) -class RiddlePersistenceService( - private val riddleEntityRepository: RiddleEntityRepository, - private val riddleMappingRepository: RiddleMappingRepository, -) { - - @Retryable(value = [ SQLException::class ], maxRetries = 5, delay = 500L, multiplier = 1.5) - @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW) - fun saveAllRiddleMapping(entities: MutableIterable) { - riddleMappingRepository.saveAll(entities) - } - - @Transactional(readOnly = false, isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW) - fun saveRiddleMapping(entity: RiddleMappingEntity) { - riddleMappingRepository.save(entity) - } - - @Transactional(readOnly = false, isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW) - fun saveRiddle(entity: RiddleEntity) { - riddleEntityRepository.save(entity) - } - -} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleReadonlyService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleReadonlyService.kt deleted file mode 100644 index 62419590d..000000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleReadonlyService.kt +++ /dev/null @@ -1,25 +0,0 @@ -package hu.bme.sch.cmsch.component.riddle - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional - -@Service -@ConditionalOnBean(RiddleComponent::class) -class RiddleReadonlyService( - private val riddleEntityRepository: RiddleEntityRepository, - private val riddleMappingRepository: RiddleMappingRepository -) { - - data class RiddleDetails(val all: Int, val solved: Int, val skipped: Int) - - @Transactional(readOnly = true) - fun getRiddleDetails(groupId: Int): RiddleDetails { - return RiddleDetails( - riddleEntityRepository.count().toInt(), - riddleMappingRepository.countAllByOwnerGroupIdAndCompletedTrue(groupId), - riddleMappingRepository.countAllByOwnerGroupIdAndCompletedTrueAndSkippedTrue(groupId), - ) - } - -} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/team/TeamService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/team/TeamService.kt index 076bb3611..a664d58ed 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/team/TeamService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/team/TeamService.kt @@ -6,7 +6,8 @@ import hu.bme.sch.cmsch.component.login.CmschUser import hu.bme.sch.cmsch.component.qrfight.QrFightService import hu.bme.sch.cmsch.component.race.DEFAULT_CATEGORY import hu.bme.sch.cmsch.component.race.RaceService -import hu.bme.sch.cmsch.component.riddle.RiddleReadonlyService +import hu.bme.sch.cmsch.component.riddle.RiddleEntityRepository +import hu.bme.sch.cmsch.component.riddle.RiddleMappingRepository import hu.bme.sch.cmsch.component.task.TasksService import hu.bme.sch.cmsch.config.OwnershipType import hu.bme.sch.cmsch.config.StartupPropertyConfig @@ -46,7 +47,8 @@ class TeamService( private val tasksService: Optional, private val formsService: Optional, private val qrFightService: Optional, - private val riddleReadonlyService: Optional, + private val riddleMappingRepository: Optional, + private val riddleEntityRepository: Optional, private val clock: TimeService, private val storageService: StorageService ) { @@ -384,13 +386,15 @@ class TeamService( } if (teamComponent.riddleStatEnabled) { - riddleReadonlyService.ifPresent { riddles -> - val details = riddles.getRiddleDetails(group.id) + riddleMappingRepository.ifPresent { repo -> + val solved = repo.countAllByOwnerGroupIdAndCompletedTrue(group.id) + val skipped = repo.countAllByOwnerGroupIdAndCompletedTrueAndSkippedTrue(group.id) + val total = riddleEntityRepository.map { it.count() }.orElse(0L) stats.add(TeamStatView( name = teamComponent.riddleStatHeader, - value1 = "${details.solved} db", - value2 = "Ebből átugrott ${details.skipped} db", - percentage = if (details.all == 0) 1f else (details.solved / details.all).toFloat(), + value1 = "$solved db", + value2 = "Ebből átugrott $skipped db", + percentage = if (total == 0L) 0f else (solved.toFloat() / total.toFloat()), navigate = "/riddle" )) } @@ -617,4 +621,4 @@ class TeamService( || file.originalFilename!!.lowercase().endsWith(".gif") ) } -} +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/StartupPropertyConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/StartupPropertyConfig.kt index 10046bb78..c486976bf 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/StartupPropertyConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/StartupPropertyConfig.kt @@ -33,12 +33,6 @@ data class StartupPropertyConfig @ConstructorBinding constructor( // Increased session val increasedSessionTime: Int, - // Microservice - val masterRole: Boolean, - val riddleMicroserviceEnabled: Boolean, - val managementToken: String, - val nodeName: String, - // Storage val storageImplementation: StorageImplementation, val storageCacheMaxAge: Long, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt index 25a964d2f..6f7f1c3e8 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt @@ -15,7 +15,11 @@ import hu.bme.sch.cmsch.component.form.ResponseRepository import hu.bme.sch.cmsch.component.news.NewsEntity import hu.bme.sch.cmsch.component.news.NewsRepository import hu.bme.sch.cmsch.component.qrfight.QrFightComponent -import hu.bme.sch.cmsch.component.riddle.* +import hu.bme.sch.cmsch.component.riddle.RiddleCategoryEntity +import hu.bme.sch.cmsch.component.riddle.RiddleComponent +import hu.bme.sch.cmsch.component.riddle.RiddleEntity +import hu.bme.sch.cmsch.component.riddle.RiddleEntityRepository +import hu.bme.sch.cmsch.component.riddle.RiddleCategoryRepository import hu.bme.sch.cmsch.component.staticpage.StaticPageEntity import hu.bme.sch.cmsch.component.staticpage.StaticPageRepository import hu.bme.sch.cmsch.component.task.* @@ -82,7 +86,6 @@ class TestConfig( private val formRepository: Optional, private val formResponseRepository: Optional, private val extraMenuRepository: ExtraMenuRepository, - private val riddleCacheManager: Optional, private val startupPropertyConfig: StartupPropertyConfig, ) { @@ -138,7 +141,6 @@ class TestConfig( if (inited) return inited = true - riddleCacheManager.ifPresent { it.resetCache(persistMapping = false, overrideMappings = false) } } private fun addForms(form: FormRepository, response: ResponseRepository) { diff --git a/backend/src/main/resources/config/application-env.properties b/backend/src/main/resources/config/application-env.properties index 32c7c2807..c130af26a 100644 --- a/backend/src/main/resources/config/application-env.properties +++ b/backend/src/main/resources/config/application-env.properties @@ -93,8 +93,6 @@ hu.bme.sch.cmsch.startup.riddle-ownership-mode=${OWNER_RIDDLE:USER} hu.bme.sch.cmsch.startup.challenge-ownership-mode=${OWNER_CHALLENGE:USER} hu.bme.sch.cmsch.startup.race-ownership-mode=${OWNER_RACE:USER} -hu.bme.sch.cmsch.startup.master-role=${MS_MASTER_ROLE:true} -hu.bme.sch.cmsch.startup.riddle-microservice-enabled=${RIDDLE_MICROSERVICE:false} -hu.bme.sch.cmsch.startup.management-token=${MANAGEMENT_TOKEN:} -hu.bme.sch.cmsch.startup.node-name=${MS_NODE_NAME:NOT_SET} hu.bme.sch.cmsch.startup.distributed-mode=${DISTRIBUTED_MODE:false} + +hu.bme.sch.cmsch.component.pushnotification.token-stale-days=${PUSH_TOKEN_STALE_DAYS:30} diff --git a/backend/src/main/resources/config/application-microservice.properties b/backend/src/main/resources/config/application-microservice.properties deleted file mode 100644 index d807ba426..000000000 --- a/backend/src/main/resources/config/application-microservice.properties +++ /dev/null @@ -1,12 +0,0 @@ -server.error.include-stacktrace=never - -server.tomcat.threads.max=${THREAD_SIZE:10} -server.tomcat.threads.min-spare=${MIN_SPEARE_THREAD:2} - -springdoc.api-docs.enabled=false -springdoc.swagger-ui.enabled=false - -hu.bme.sch.cmsch.startup.master-role=false -hu.bme.sch.cmsch.startup.riddle-microservice-enabled=${RIDDLE_MICROSERVICE:true} -hu.bme.sch.cmsch.startup.management-token=${MANAGEMENT_TOKEN:} -hu.bme.sch.cmsch.startup.node-name=${MS_NODE_NAME:NOT_SET} diff --git a/backend/src/main/resources/config/application.properties b/backend/src/main/resources/config/application.properties index ba490bb57..284cd49dd 100644 --- a/backend/src/main/resources/config/application.properties +++ b/backend/src/main/resources/config/application.properties @@ -43,12 +43,6 @@ hu.bme.sch.cmsch.startup.profile-qr-prefix=${QR_PREFIX:KIRDEV_} hu.bme.sch.cmsch.startup.profile-salt=uZpWi5XksLnjbDHq3OuQrpFeMpTjhwIBivMWY5DxbwE1G3MFnA8KfBzUJbLVJYHD1uuYIVxrFuXkazhy8KxtrkEqexLOv8h2eqMBJnByg2GK1cb6VVu3e77T hu.bme.sch.cmsch.startup.mailgun-token=${MAILGUN_TOKEN:no} -hu.bme.sch.cmsch.startup.master-role=true -hu.bme.sch.cmsch.startup.distributed-mode=false -hu.bme.sch.cmsch.startup.riddle-microservice-enabled=false -hu.bme.sch.cmsch.startup.management-token=replace_to_string -hu.bme.sch.cmsch.startup.node-name=core - server.servlet.session.cookie.same-site=lax server.servlet.session.cookie.max-age=172800 hu.bme.sch.cmsch.startup.session-validity-seconds=172800 @@ -166,6 +160,8 @@ hu.bme.sch.cmsch.startup.riddle-ownership-mode=USER hu.bme.sch.cmsch.startup.challenge-ownership-mode=GROUP hu.bme.sch.cmsch.startup.race-ownership-mode=USER +hu.bme.sch.cmsch.component.pushnotification.token-stale-days=30 + spring.jackson.serialization.indent-output=false diff --git a/helm/cmsch/templates/cmsch-config.yml b/helm/cmsch/templates/cmsch-config.yml index bfe9f5aab..6dc7c2f5e 100644 --- a/helm/cmsch/templates/cmsch-config.yml +++ b/helm/cmsch/templates/cmsch-config.yml @@ -68,5 +68,6 @@ data: RIDDLE_MICROSERVICE: {{ .Values.riddle.microservice | quote }} MANAGEMENT_TOKEN: {{ .Values.riddle.managementToken | quote }} MS_NODE_NAME: {{ .Values.riddle.msNodeName | quote }} + PUSH_TOKEN_STALE_DAYS: {{ .Values.pushnotification.tokenStaleDays | quote }} METRICS_NAME: {{ .Release.Name | quote }} SWAGGER_ENABLED: {{ .Values.swaggerEnabled | quote }} diff --git a/helm/cmsch/values.yaml b/helm/cmsch/values.yaml index caf6d90dc..bced62e64 100644 --- a/helm/cmsch/values.yaml +++ b/helm/cmsch/values.yaml @@ -170,6 +170,9 @@ riddle: managementToken: "" msNodeName: NOT_SET +pushnotification: + tokenStaleDays: 30 + startupProbe: failureThreshold: 60 periodSeconds: 5