From 04e11b0fea72a2273c90a7f966aca79595add129 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Mon, 26 Feb 2024 00:23:51 -0500 Subject: [PATCH] [+] Keychip session --- .../icu/samnyan/aqua/AquaServerApplication.kt | 3 +- .../icu/samnyan/aqua/net/UserRegistrar.kt | 4 +- .../icu/samnyan/aqua/net/db/AquaNetUser.kt | 24 +++++-- .../icu/samnyan/aqua/net/games/Maimai2.kt | 12 ++-- .../java/icu/samnyan/aqua/net/games/Models.kt | 11 ++- .../icu/samnyan/aqua/sega/allnet/Keychip.kt | 4 ++ .../aqua/sega/allnet/KeychipSession.kt | 69 +++++++++++++++++++ 7 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt diff --git a/src/main/java/icu/samnyan/aqua/AquaServerApplication.kt b/src/main/java/icu/samnyan/aqua/AquaServerApplication.kt index 29e02745..78da9a63 100644 --- a/src/main/java/icu/samnyan/aqua/AquaServerApplication.kt +++ b/src/main/java/icu/samnyan/aqua/AquaServerApplication.kt @@ -1,13 +1,14 @@ package icu.samnyan.aqua -import icu.samnyan.aqua.net.components.EmailService import icu.samnyan.aqua.sega.aimedb.AimeDbServer import icu.samnyan.aqua.spring.util.AutoChecker import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.scheduling.annotation.EnableScheduling import java.io.File @SpringBootApplication +@EnableScheduling class AquaServerApplication /** diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index a30cefe4..52ed2841 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -3,7 +3,7 @@ package icu.samnyan.aqua.net import ext.* import icu.samnyan.aqua.net.components.* import icu.samnyan.aqua.net.db.* -import icu.samnyan.aqua.net.db.AquaUserValidator.Companion.SETTING_FIELDS +import icu.samnyan.aqua.net.db.AquaUserServices.Companion.SETTING_FIELDS import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.model.Card @@ -26,7 +26,7 @@ class UserRegistrar( val confirmationRepo: EmailConfirmationRepo, val cardRepo: CardRepository, val cardService: CardService, - val validator: AquaUserValidator, + val validator: AquaUserServices, val emailProps: EmailProperties ) { companion object { diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt index 53c552ac..9a7b03fb 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore import ext.Str import ext.isValidEmail import ext.minus +import icu.samnyan.aqua.sega.allnet.KeychipSession import icu.samnyan.aqua.sega.general.model.Card import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository @@ -59,7 +60,17 @@ class AquaNetUser( // One user can have multiple cards @OneToMany(mappedBy = "aquaUser", cascade = [CascadeType.ALL]) - var cards: MutableList = mutableListOf() + var cards: MutableList = mutableListOf(), + + // Each user can have one keychip (if the user owns a cabinet) + @JsonIgnore + @Column(nullable = true, length = 32, unique = true) + var keychip: Str? = null, + + // Each user's keychip can have multiple sessions + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL]) + var keychipSessions: MutableList = mutableListOf(), ) : Serializable { val computedName get() = displayName.ifEmpty { username } } @@ -69,9 +80,7 @@ interface AquaNetUserRepo : JpaRepository { fun findByAuId(auId: Long): AquaNetUser? fun findByEmailIgnoreCase(email: String): AquaNetUser? fun findByUsernameIgnoreCase(username: String): AquaNetUser? - - fun byName(username: Str, callback: (AquaNetUser) -> T) = - findByUsernameIgnoreCase(username)?.let(callback) ?: (404 - "User not found") + fun findByKeychip(keychip: String): AquaNetUser? } data class SettingField( @@ -85,12 +94,12 @@ data class SettingField( * throw an ApiException if the field is invalid. */ @Service -class AquaUserValidator( +class AquaUserServices( val userRepo: AquaNetUserRepo, val hasher: PasswordEncoder, ) { companion object { - val SETTING_FIELDS = AquaUserValidator::class.functions + val SETTING_FIELDS = AquaUserServices::class.functions .filter { it.name.startsWith("check") } .map { val name = it.name.removePrefix("check").replaceFirstChar { c -> c.lowercase() } @@ -99,6 +108,9 @@ class AquaUserValidator( } } + fun byName(username: Str, callback: (AquaNetUser) -> T) = + userRepo.findByUsernameIgnoreCase(username)?.let(callback) ?: (404 - "User not found") + fun checkUsername(username: Str) = username.apply { // Check if username is valid if (length < 2) 400 - "Username must be at least 2 letters" diff --git a/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt b/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt index 1e333a43..bb322fb6 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt @@ -4,7 +4,7 @@ import ext.API import ext.RP import ext.Str import ext.minus -import icu.samnyan.aqua.net.db.AquaNetUserRepo +import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserDataRepository import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserGeneralDataRepository import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserPlaylogRepository @@ -13,14 +13,13 @@ import org.springframework.web.bind.annotation.RestController @RestController @API("api/v2/game/maimai2") class Maimai2( - val user: AquaNetUserRepo, + val us: AquaUserServices, val userPlaylogRepository: UserPlaylogRepository, val userDataRepository: UserDataRepository, val userGeneralDataRepository: UserGeneralDataRepository -) +): GameApiController { - @API("trend") - fun trend(@RP username: Str): List = user.byName(username) { u -> + override fun trend(@RP username: Str): List = us.byName(username) { u -> // O(n log n) sort val d = userPlaylogRepository.findByUser_Card_ExtId(u.ghostCard.extId).sortedBy { it.playDate }.toList() @@ -45,8 +44,7 @@ class Maimai2( 98.0 to "S+", 97.0 to "S").map { (k, v) -> (k * 10000).toInt() to v } - @API("user-summary") - fun userSummary(@RP username: Str) = user.byName(username) { u -> + override fun userSummary(@RP username: Str) = us.byName(username) { u -> // Summary values: total plays, player rating, server-wide ranking // number of each rank, max combo, number of full combo, number of all perfect val user = userDataRepository.findByCard(u.ghostCard) ?: (404 - "User not found") diff --git a/src/main/java/icu/samnyan/aqua/net/games/Models.kt b/src/main/java/icu/samnyan/aqua/net/games/Models.kt index e385b2db..c93fd688 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Models.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Models.kt @@ -1,5 +1,7 @@ package icu.samnyan.aqua.net.games +import ext.API + data class TrendOut(val date: String, val rating: Int, val plays: Int) data class GenericGamePlaylog( @@ -34,4 +36,11 @@ data class GenericGameSummary( val ratingComposition: Map, val recent: List -) \ No newline at end of file +) + +interface GameApiController { + @API("trend") + fun trend(username: String): List + @API("user-summary") + fun userSummary(username: String): GenericGameSummary +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/Keychip.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/Keychip.kt index fb7e60a4..f88ff798 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/Keychip.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/Keychip.kt @@ -8,6 +8,10 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository import java.io.Serializable +/** + * This is the old method of securing requests - a keychip whitelist, + * it's kept here only for backwards compatibility. + */ @Entity @Table(name = "allnet_keychips") class Keychip( diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt new file mode 100644 index 00000000..89e3c277 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt @@ -0,0 +1,69 @@ +package icu.samnyan.aqua.sega.allnet + +import icu.samnyan.aqua.net.db.AquaNetUser +import jakarta.persistence.* +import org.slf4j.LoggerFactory +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Repository +import org.springframework.stereotype.Service +import java.security.SecureRandom + +/** + * This is a one-to-many mapping of keychip to session token. + */ +@Entity +@Table(name = "allnet_keychip_sessions", indexes = [ + Index(name = "idx_last_use", columnList = "lastUse") +]) +class KeychipSession( + @ManyToOne + @JoinColumn(name = "au_id") + var user: AquaNetUser = AquaNetUser(), + + @Id + @Column(length = 32) + val token: String = genUrlSafeToken(32), + + @Column(nullable = false) + val lastUse: Long = System.currentTimeMillis() +) + + +val urlSafeChars = ('a'..'z') + ('A'..'Z') + ('0'..'9') + listOf('-', '_', '.', '~') + +fun genUrlSafeToken(length: Int): String { + val random = SecureRandom() + return (1..length) + .map { urlSafeChars[random.nextInt(urlSafeChars.size)] } + .joinToString("") +} + +@Repository("KeychipSessionRepo") +interface KeychipSessionRepo : JpaRepository { + fun findByToken(token: String): KeychipSession? + fun deleteAllByLastUseBefore(expire: Long) +} + +/** + * Service to regularly delete unused keychip sessions. + */ +@Service +class KeychipSessionService( + val keychipSessionRepo: KeychipSessionRepo, + val props: AllNetProps +) { + val logger = LoggerFactory.getLogger(KeychipSessionService::class.java) + + @Scheduled(fixedDelayString = "\${allnet.server.keychip-ses-clean-interval}") + fun cleanup() { + logger.info("!!! Keychip session cleanup !!!") + val expire = System.currentTimeMillis() - props.keychipSesExpire + keychipSessionRepo.deleteAllByLastUseBefore(expire) + } + + fun new(user: AquaNetUser): KeychipSession { + val session = KeychipSession(user = user) + return keychipSessionRepo.save(session) + } +} \ No newline at end of file