-
Notifications
You must be signed in to change notification settings - Fork 6
Add Kir-Pay integration #1045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Add Kir-Pay integration #1045
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package hu.bme.sch.cmsch.component.kirpay | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonView | ||
| import hu.bme.sch.cmsch.dto.FullDetails | ||
| import hu.bme.sch.cmsch.model.RoleType | ||
| import hu.bme.sch.cmsch.util.getUserOrNull | ||
| import hu.bme.sch.cmsch.util.isAvailableForRole | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnBean | ||
| import org.springframework.http.HttpStatus | ||
| import org.springframework.http.ResponseEntity | ||
| import org.springframework.security.core.Authentication | ||
| import org.springframework.web.bind.annotation.GetMapping | ||
| import org.springframework.web.bind.annotation.RequestMapping | ||
| import org.springframework.web.bind.annotation.RestController | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api") | ||
| @ConditionalOnBean(KirPayComponent::class) | ||
| class KirPayApiController( | ||
| private val kirPayService: KirPayService, | ||
| private val kirPayComponent: KirPayComponent | ||
| ) { | ||
|
|
||
| @JsonView(FullDetails::class) | ||
| @GetMapping("/kirpay-leaderboard") | ||
| fun leaderboard(auth: Authentication?): ResponseEntity<KirPayLeaderboardView> { | ||
| val user = auth?.getUserOrNull() | ||
|
|
||
| if (!kirPayComponent.leaderboardEnabled) | ||
| return ResponseEntity.ok(KirPayLeaderboardView()) | ||
|
|
||
| if (!kirPayComponent.minRole.isAvailableForRole(user?.role ?: RoleType.GUEST)) | ||
| return ResponseEntity.status(HttpStatus.FORBIDDEN).build() | ||
|
|
||
| var entries = kirPayService.getConsumptionLeaderboard() | ||
|
|
||
| val limit = kirPayComponent.leaderboardMaxEntries.toInt() | ||
| if (limit >= 0) { | ||
| entries = entries.take(limit) | ||
| } | ||
|
|
||
| return ResponseEntity.ok(KirPayLeaderboardView(entries = entries)) | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package hu.bme.sch.cmsch.component.kirpay | ||
|
|
||
| import hu.bme.sch.cmsch.component.ComponentBase | ||
| import hu.bme.sch.cmsch.service.ControlPermissions | ||
| import hu.bme.sch.cmsch.setting.* | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty | ||
| import org.springframework.core.env.Environment | ||
| import org.springframework.stereotype.Service | ||
|
|
||
| @Service | ||
| @ConditionalOnBooleanProperty(value = ["hu.bme.sch.cmsch.component.load.kirpay"]) | ||
| class KirPayComponent( | ||
| componentSettingService: ComponentSettingService, | ||
| env: Environment | ||
| ) : ComponentBase( | ||
| componentSettingService, | ||
| "kirpay", | ||
| "/kirpay-leaderboard", | ||
| "Kir-Pay Toplista", | ||
|
Check failure on line 19 in backend/src/main/kotlin/hu/bme/sch/cmsch/component/kirpay/KirPayComponent.kt
|
||
| ControlPermissions.PERMISSION_CONTROL_LEADERBOARD, | ||
| listOf(), | ||
| env | ||
| ) { | ||
|
|
||
| val leaderboardGroup by SettingGroup(fieldName = "Toplista") | ||
|
|
||
| final var title by StringSettingRef("Kir-Pay Toplista", | ||
| fieldName = "Lap címe", | ||
| description = "Ez jelenik meg a böngésző címsorában") | ||
|
|
||
| final override var menuDisplayName by StringSettingRef("Kir-Pay Toplista", serverSideOnly = true, | ||
| fieldName = "Menü neve", description = "Ez lesz a neve a menünek") | ||
|
|
||
| final override var minRole by MinRoleSettingRef(setOf(), | ||
| fieldName = "Jogosultságok", description = "Melyik roleokkal nyitható meg az oldal") | ||
|
|
||
| /// ------------------------------------------------------------------------------------------------------------------- | ||
|
|
||
| val connectionGroup by SettingGroup(fieldName = "Kapcsolat") | ||
|
|
||
| var kirPayBackendUrl by StringSettingRef("http://localhost:3001/v1/api/admin", serverSideOnly = true, | ||
| fieldName = "Kir-Pay backend URL", description = "A Kir-Pay backend admin API URL-je") | ||
|
|
||
| var kirPayBackendToken by StringSettingRef(serverSideOnly = true, fieldName = "Kir-Pay backend token", | ||
| description = "Basic auth token a Kir-Pay admin API-hoz") | ||
|
|
||
| /// ------------------------------------------------------------------------------------------------------------------- | ||
|
|
||
| val displayGroup by SettingGroup(fieldName = "Kijelzés") | ||
|
|
||
| var leaderboardEnabled by BooleanSettingRef(true, | ||
| fieldName = "Toplista aktív", description = "A toplista leküldésre kerül") | ||
|
|
||
| var leaderboardMaxEntries by NumberSettingRef(50, fieldName = "Toplista sorainak száma", | ||
| description = "Hány felhasználót mutasson, -1 = az összeset") | ||
|
|
||
| var kirPayCurrency by StringSettingRef("JMF", fieldName = "A megjelenített Kir-Pay valuta") | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package hu.bme.sch.cmsch.component.kirpay | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonView | ||
| import hu.bme.sch.cmsch.dto.FullDetails | ||
|
|
||
| data class KirPayLeaderboardEntry( | ||
| @field:JsonView(FullDetails::class) | ||
| val name: String, | ||
|
|
||
| @field:JsonView(FullDetails::class) | ||
| val itemCount: Long | ||
| ) | ||
|
|
||
| data class KirPayLeaderboardView( | ||
| @field:JsonView(FullDetails::class) | ||
| val entries: List<KirPayLeaderboardEntry> = listOf() | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package hu.bme.sch.cmsch.component.kirpay | ||
|
|
||
| import org.slf4j.LoggerFactory | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnBean | ||
| import org.springframework.http.client.reactive.ReactorClientHttpConnector | ||
| import org.springframework.scheduling.annotation.Scheduled | ||
| import org.springframework.stereotype.Service | ||
| import org.springframework.web.reactive.function.client.WebClient | ||
| import org.springframework.web.reactive.function.client.WebClientResponseException | ||
| import org.springframework.web.reactive.function.client.bodyToMono | ||
| import reactor.netty.http.client.HttpClient | ||
| import java.time.Duration | ||
| import java.util.* | ||
|
|
||
| @Service | ||
| @ConditionalOnBean(KirPayComponent::class) | ||
| class KirPayService( | ||
| private val kirPayComponent: KirPayComponent, | ||
| webClientBuilder: WebClient.Builder | ||
| ) { | ||
|
|
||
| private val log = LoggerFactory.getLogger(javaClass) | ||
|
|
||
| @Volatile | ||
| private var cachedLeaderboard: List<KirPayLeaderboardEntry>? = null | ||
| private val leaderboardLock = Any() | ||
|
|
||
| private val kirPayClient: WebClient by lazy { | ||
| val httpClient = HttpClient.create() | ||
| .responseTimeout(Duration.ofSeconds(15)) | ||
| webClientBuilder | ||
| .clientConnector(ReactorClientHttpConnector(httpClient)) | ||
| .baseUrl(kirPayComponent.kirPayBackendUrl) | ||
| .defaultHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString( | ||
| kirPayComponent.kirPayBackendToken.toByteArray() | ||
| )) | ||
| .build() | ||
| } | ||
|
|
||
| @Scheduled(fixedRate = 1000L * 60 * 60 * 10) | ||
| fun refreshCache() { | ||
| if (!kirPayComponent.leaderboardEnabled) return | ||
| val refreshed = fetchConsumptionLeaderboard() | ||
| synchronized(leaderboardLock) { | ||
| cachedLeaderboard = refreshed | ||
| } | ||
| log.info("Kir-Pay consumption leaderboard cache refreshed, {} entries", refreshed.size) | ||
| } | ||
|
|
||
| fun getConsumptionLeaderboard(): List<KirPayLeaderboardEntry> { | ||
| if (!kirPayComponent.leaderboardEnabled) return listOf() | ||
| cachedLeaderboard?.let { return it } | ||
| synchronized(leaderboardLock) { | ||
| cachedLeaderboard?.let { return it } | ||
| val result = fetchConsumptionLeaderboard() | ||
| cachedLeaderboard = result | ||
| return result | ||
| } | ||
| } | ||
|
|
||
| fun getBalanceByEmail(email: String?): Long? { | ||
| if (email.isNullOrBlank()) return null | ||
|
|
||
| return try { | ||
| val response = kirPayClient.get() | ||
| .uri("/accounts-by-email/{email}", email) | ||
| .retrieve() | ||
| .bodyToMono<Map<String, Any>>() | ||
| .block() | ||
|
|
||
| response?.get("balance")?.let { (it as Number).toLong() } | ||
| } catch (e: WebClientResponseException.NotFound) { | ||
| null | ||
| } catch (e: Exception) { | ||
| log.error("Failed to fetch Kir-Pay balance", e) | ||
| null | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| private fun fetchConsumptionLeaderboard(): List<KirPayLeaderboardEntry> { | ||
| return try { | ||
| val response = kirPayClient.get() | ||
| .uri("/consumption-leaderboard") | ||
| .retrieve() | ||
| .bodyToMono<List<Map<String, Any>>>() | ||
| .block() | ||
| ?: listOf() | ||
|
|
||
| return response.map { | ||
| // Some gorgeous Kotlin code! | ||
| val name = it["name"] as? String ?: return@map null | ||
| val itemCount = it["itemCount"] as? Number ?: return@map null | ||
|
|
||
| KirPayLeaderboardEntry(name = name, itemCount = itemCount.toLong()) | ||
| }.filterNotNull() | ||
|
|
||
| } catch (e: Exception) { | ||
| log.error("Failed to fetch Kir-Pay consumption leaderboard", e) | ||
| listOf() | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -142,6 +142,15 @@ class ProfileComponent( | |
| fieldName = "Helyzet lejárata", strictConversion = false, | ||
| description = "Ennyi ideig (másodpercben) jelenjen meg a helyzet az utolsó frissítés után") | ||
|
|
||
| /// ------------------------------------------------------------------------------------------------------------------- | ||
| /// | ||
|
|
||
| val kirPayGroup by SettingGroup(fieldName = "Kir-Pay", | ||
| description = "A Kir-Pay komponensnek is be kell kapcsolva lennie, hogy az itteni beállítások működjenek") | ||
|
|
||
| var showKirPayBalance by BooleanSettingRef(true, | ||
| fieldName = "Kir-Pay egyenleg látható", description = "Megjelenjen-e a Kir-Pay egyenleg a profilban") | ||
|
Comment on lines
+148
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win This creates a second independent
🤖 Prompt for AI Agents |
||
|
|
||
| /// ------------------------------------------------------------------------------------------------------------------- | ||
|
|
||
| val tokenGoalGroup by SettingGroup(fieldName = "Token cél megjelenítése", | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.