diff --git a/AquaNet/src/components/settings/UserSettings.svelte b/AquaNet/src/components/settings/UserSettings.svelte index 29faee9f..1220b304 100644 --- a/AquaNet/src/components/settings/UserSettings.svelte +++ b/AquaNet/src/components/settings/UserSettings.svelte @@ -1,10 +1,10 @@ - + {#if !submitting && !error && me}
@@ -171,6 +209,16 @@
+
+ {t('settings.profile.delete-title')} + {t('settings.profile.delete-description')} +
+ +
+
{/if} @@ -234,6 +282,19 @@ object-fit: cover aspect-ratio: 1 + .danger-zone + margin-top: 1.5rem + padding-top: 1.5rem + border-top: 1px solid vars.$ov-lighter + + .desc + opacity: 0.6 + + button + display: flex + align-items: center + gap: 0.5rem + .cropper-container diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index 11b296fb..8e115ae9 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -274,6 +274,11 @@ export const EN_REF_SETTINGS = { 'settings.profile.unset': 'Unset', 'settings.profile.logout': 'Log out', 'settings.profile.unchanged': 'Unchanged', + 'settings.profile.delete-title': 'Delete account', + 'settings.profile.delete-description': 'Download a backup, then permanently delete your account and all associated game data.', + 'settings.profile.delete': 'Delete account', + 'settings.profile.delete-confirm-title': 'Permanently delete your account?', + 'settings.profile.delete-confirm-message': 'A backup of all game data will be downloaded first. After that, your account and all associated game data will be permanently deleted. This cannot be undone.', 'settings.export': 'Export Player Data', 'settings.batchManualExport': "Export in Batch Manual (for Tachi)", 'settings.cabNotice': "These settings only apply for your keychip. If you're playing on someone else's setup, please ask them to change these settings.", diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts index ca416d36..33b8555b 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -287,6 +287,11 @@ const zhSettings: typeof EN_REF_SETTINGS = { 'settings.profile.unset': '未设置', 'settings.profile.logout': '登出', 'settings.profile.unchanged': '未更改', + 'settings.profile.delete-title': '删除账户', + 'settings.profile.delete-description': '下载备份后,永久删除您的账户及所有关联游戏数据。', + 'settings.profile.delete': '删除账户', + 'settings.profile.delete-confirm-title': '永久删除您的账户?', + 'settings.profile.delete-confirm-message': '系统会先下载所有游戏数据的备份,然后永久删除您的账户及所有关联游戏数据。此操作无法撤销。', 'settings.export': '导出玩家数据', 'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi)", 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置', diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index 7b1e41da..a6187a65 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -14,6 +14,8 @@ import type { } from './generalTypes' import type { GameName } from './scoring' +export type ExportGameName = GameName | 'diva' + interface ExtReqInit extends RequestInit { params?: { [index: string]: string } json?: any @@ -203,6 +205,8 @@ export const USER = { ensureLoggedIn, changeRegion: (regionId: number) => post('/api/v2/user/change-region', { regionId }), + deleteAccount: () => + post('/api/v2/user/delete-account'), } export const USERBOX = { @@ -236,7 +240,7 @@ export const GAME = { post(`/api/v2/game/${game}/ranking`, typeof page === "number" ? { page } : {}), changeName: (game: GameName, newName: string): Promise<{ newName: string }> => post(`/api/v2/game/${game}/change-name`, { newName }), - export: (game: GameName): Promise> => + export: (game: ExportGameName): Promise> => post(`/api/v2/game/${game}/export`), import: (game: GameName, data: any): Promise> => post(`/api/v2/game/${game}/import`, {}, { json: data }), diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index 038f9059..33ec414f 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -2,25 +2,40 @@ package icu.samnyan.aqua.net import ext.* import icu.samnyan.aqua.net.components.* -import icu.samnyan.aqua.net.db.AquaNetUser -import icu.samnyan.aqua.net.db.AquaNetUserRepo -import icu.samnyan.aqua.net.db.AquaUserServices -import icu.samnyan.aqua.net.db.EmailConfirmationRepo -import icu.samnyan.aqua.net.db.ResetPasswordRepo +import icu.samnyan.aqua.net.db.AquaNetUser +import icu.samnyan.aqua.net.db.AquaNetUserRepo +import icu.samnyan.aqua.net.db.AquaUserServices +import icu.samnyan.aqua.net.db.EmailConfirmationRepo +import icu.samnyan.aqua.net.db.ResetPasswordRepo import icu.samnyan.aqua.net.utils.AquaNetProps import icu.samnyan.aqua.net.utils.PathProps import icu.samnyan.aqua.net.utils.SUCCESS -import icu.samnyan.aqua.sega.allnet.UserKeychip -import icu.samnyan.aqua.sega.allnet.UserKeychipRepo -import icu.samnyan.aqua.sega.general.dao.CardRepository -import icu.samnyan.aqua.sega.general.model.CardStatus -import jakarta.servlet.http.HttpServletRequest -import org.slf4j.LoggerFactory -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.multipart.MultipartFile -import java.time.Instant -import kotlin.io.path.writeBytes +import icu.samnyan.aqua.sega.allnet.UserKeychip +import icu.samnyan.aqua.sega.allnet.UserKeychipRepo +import icu.samnyan.aqua.sega.chusan.model.Chu3Repos +import icu.samnyan.aqua.sega.diva.DivaRepos +import icu.samnyan.aqua.sega.diva.model.db.userdata.PlayerProfile +import icu.samnyan.aqua.sega.general.dao.CardRepository +import icu.samnyan.aqua.sega.general.model.Card +import icu.samnyan.aqua.sega.general.model.CardStatus +import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos +import icu.samnyan.aqua.sega.ongeki.OngekiUserRepos +import icu.samnyan.aqua.sega.wacca.model.db.WaccaRepos +import jakarta.servlet.http.HttpServletRequest +import jakarta.transaction.Transactional +import org.slf4j.LoggerFactory +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile +import java.nio.file.Files +import java.nio.file.Path +import java.time.Instant +import kotlin.io.path.Path +import kotlin.io.path.deleteIfExists +import kotlin.io.path.exists +import kotlin.io.path.name +import kotlin.io.path.writeBytes @RestController @API("/api/v2/user") @@ -39,7 +54,8 @@ class UserRegistrar( val emailProps: EmailProperties, val userKeychipRepo: UserKeychipRepo, val aquaNetProps: AquaNetProps, - final val paths: PathProps + final val paths: PathProps, + val accountDeletion: AccountDeletionService, ) { val portraitPath = paths.aquaNetPortrait.path() @@ -327,14 +343,105 @@ class UserRegistrar( @API("/change-region") @Doc("Change the region of the user.", "Success message") - suspend fun changeRegion(@RP token: Str, @RP regionId: Str) = jwt.auth(token) { u -> + suspend fun changeRegion(@RP token: Str, @RP regionId: Str) = jwt.auth(token) { u -> // Check if the region is valid (between 1 and 47) val r = regionId.toIntOrNull() ?: (400 - "Invalid region") if (r !in 1..47) 400 - "Invalid region" async { userRepo.save(u.apply { region = r.toString() }) } - - SUCCESS - } -} + + SUCCESS + } + + @API("/delete-account") + @Doc("Permanently delete the current user's account and all associated game data.", "Success message") + fun deleteAccount(@RP token: Str) = jwt.auth(token) { user -> + val cleanup = accountDeletion.deleteDatabaseData(user.auId) + accountDeletion.deleteUploadedFiles(cleanup) + SUCCESS + } +} + +data class AccountFileCleanup( + val profilePicture: String?, + val gameUserId: Long, + val divaScreenshots: List, +) + +@Service +class AccountDeletionService( + val userRepo: AquaNetUserRepo, + val cardRepo: CardRepository, + val mai2: Mai2Repos, + val chu3: Chu3Repos, + val ongeki: OngekiUserRepos, + val wacca: WaccaRepos, + val diva: DivaRepos, + val paths: PathProps, +) { + companion object { + val log = logger() + } + + @Transactional + fun deleteDatabaseData(auId: Long): AccountFileCleanup { + val user = userRepo.findByAuId(auId) ?: (404 - "User not found") + val ghostCard = user.ghostCard + val divaScreenshots = deleteDiva(ghostCard) + + mai2.userData.findByCard(ghostCard)?.let { mai2.userData.delete(it) } + chu3.userData.findByCard(ghostCard)?.let { chu3.userData.delete(it) } + ongeki.data.findByCard(ghostCard)?.let { ongeki.data.delete(it) } + wacca.user.findByCard(ghostCard)?.let { wacca.user.delete(it) } + chu3.userLoginBonus.deleteAll(chu3.userLoginBonus.findByUser(ghostCard.extId.toInt())) + + // Keep Hibernate's managed state consistent with the database-level ON DELETE SET NULL. + // Hibernate 7 rejects the flush if a managed card still references the deleted user. + val linkedCards = cardRepo.findAllByAquaUserAuId(auId) + linkedCards.forEach { it.aquaUser = null } + cardRepo.saveAllAndFlush(linkedCards) + + userRepo.delete(user) + userRepo.flush() + cardRepo.delete(ghostCard) + cardRepo.flush() + + log.info("Deleted account and game data for user $auId") + return AccountFileCleanup(user.profilePicture, ghostCard.extId, divaScreenshots) + } + + private fun deleteDiva(card: Card): List { + val profile: PlayerProfile = diva.profile.findByPdId(card.extId).orElse(null) ?: return emptyList() + val screenshots = diva.screenShot.findByPdId(profile) + + diva.profile.delete(profile) + + return screenshots.map { it.fileName } + } + + fun deleteUploadedFiles(cleanup: AccountFileCleanup) { + try { + cleanup.profilePicture + ?.takeIf { it.isNotBlank() && Path(it).name == it } + ?.let { (Path(paths.aquaNetPortrait) / it).deleteIfExists() } + + deleteFilesStartingWith(Path(paths.mai2Portrait), "${cleanup.gameUserId}-") + deleteFilesStartingWith(Path(paths.mai2Plays), "${cleanup.gameUserId}-") + deleteFilesStartingWith(Path("data/tmp"), "${cleanup.gameUserId}-") + cleanup.divaScreenshots + .filter { Path(it).name == it } + .forEach { (Path("data") / it).deleteIfExists() } + } catch (e: Exception) { + log.error("Failed to delete one or more uploaded files for deleted user ${cleanup.gameUserId}", e) + } + } + + private fun deleteFilesStartingWith(directory: Path, prefix: String) { + if (!directory.exists()) return + Files.list(directory).use { files -> + files.filter { it.fileName.toString().startsWith(prefix) } + .forEach { it.deleteIfExists() } + } + } +} diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailConfirmation.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailConfirmation.kt index 9a324647..ec347b8d 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailConfirmation.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailConfirmation.kt @@ -1,7 +1,9 @@ package icu.samnyan.aqua.net.db import jakarta.persistence.* -import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.JpaRepository +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import org.springframework.stereotype.Repository import java.io.Serializable import java.time.Instant @@ -20,9 +22,10 @@ class EmailConfirmation( @Column(nullable = false) var createdAt: Instant = Instant.now(), - // Linking to the AquaNetUser - @ManyToOne - @JoinColumn(name = "auId", referencedColumnName = "auId") + // Linking to the AquaNetUser + @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "auId", referencedColumnName = "auId") var aquaNetUser: AquaNetUser = AquaNetUser() ) : Serializable @@ -30,4 +33,4 @@ class EmailConfirmation( interface EmailConfirmationRepo : JpaRepository { fun findByToken(token: String): EmailConfirmation? fun findByAquaNetUserAuId(auId: Long): List -} \ No newline at end of file +} diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt index 98a0281c..729c24aa 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt @@ -2,6 +2,8 @@ package icu.samnyan.aqua.net.db import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import org.springframework.stereotype.Repository import java.io.Serializable import java.time.Instant @@ -22,6 +24,7 @@ class ResetPassword( // Linking to the AquaNetUser @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "auId", referencedColumnName = "auId") var aquaNetUser: AquaNetUser = AquaNetUser() ) : Serializable @@ -30,4 +33,4 @@ class ResetPassword( interface ResetPasswordRepo : JpaRepository { fun findByToken(token: String): ResetPassword? fun findByAquaNetUserAuId(auId: Long): List -} \ No newline at end of file +} diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt index f3f24d52..0b7220a7 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt @@ -2,6 +2,8 @@ package icu.samnyan.aqua.net.db import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import org.springframework.stereotype.Repository import java.io.Serializable import java.time.Instant @@ -22,6 +24,7 @@ class SessionToken( // Linking to the AquaNetUser @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "auId", referencedColumnName = "auId") var aquaNetUser: AquaNetUser = AquaNetUser() ) : Serializable 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 db2b2b5a..c80ba967 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -69,23 +69,23 @@ class AquaNetUser( // Email confirmation var emailConfirmed: Boolean = false, - @OneToOne(cascade = [CascadeType.ALL]) + @OneToOne(cascade = [CascadeType.PERSIST, CascadeType.MERGE]) @JoinColumn(name = "ghostCard", unique = true, nullable = false) var ghostCard: Card = Card(), // One user can have multiple cards - @OneToMany(mappedBy = "aquaUser", cascade = [CascadeType.ALL]) + @OneToMany(mappedBy = "aquaUser", cascade = [CascadeType.PERSIST, CascadeType.MERGE]) var cards: MutableList = mutableListOf(), // Each user can have multiple keychips (if the user owns cabinets) @JsonIgnore - @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL]) - var keychips: MutableList = mutableListOf(), + @OneToMany(mappedBy = "user", cascade = [CascadeType.PERSIST, CascadeType.MERGE]) + var keychips: MutableList = mutableListOf(), // Each user's keychip can have multiple sessions @JsonIgnore - @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL]) - var keychipSessions: MutableList = mutableListOf(), + @OneToMany(mappedBy = "user", cascade = [CascadeType.PERSIST, CascadeType.MERGE]) + var keychipSessions: MutableList = mutableListOf(), @Column(nullable = false) var canModifyKeychips: Boolean = false, diff --git a/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt b/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt index d2fd3dba..56eb8b1b 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt @@ -3,6 +3,7 @@ package icu.samnyan.aqua.net.games.chu3 import ext.API import ext.returns import ext.vars +import icu.samnyan.aqua.net.games.ExportOptions import icu.samnyan.aqua.net.games.IExportClass import icu.samnyan.aqua.net.games.ImportClass import icu.samnyan.aqua.net.games.ImportController @@ -12,6 +13,7 @@ import icu.samnyan.aqua.sega.chusan.model.userdata.* import org.springframework.web.bind.annotation.RestController import kotlin.reflect.full.declaredMembers +@Suppress("UNCHECKED_CAST") @RestController @API("api/v2/game/chu3") class Chu3Import( @@ -22,7 +24,7 @@ class Chu3Import( it.name.replace("List", "").lowercase() }, exportRepos = Chu3DataExport::class.vars() - .filter { f -> f.name !in setOf("gameId", "userData") } + .filter { f -> f.name !in setOf("gameId", "userData", "userLoginBonusList") } .associateWith { Chu3Repos::class.declaredMembers .filter { f -> f returns Chu3UserLinked::class } .firstOrNull { f -> f.name == it.name || f.name == it.name.replace("List", "") } @@ -44,7 +46,22 @@ class Chu3Import( // "chuni_profile_emoney" to ImportClass(UserEmoney::class), // "chuni_profile_overpower" to ImportClass(UserOverpower::class), // "chuni_profile_recent_rating" to ImportClass(UserRecentRating::class), - ) + ), + customExporters = mapOf( + Chu3DataExport::userLoginBonusList to { user: Chu3UserData, _: ExportOptions -> + repos.userLoginBonus.findByUser(user.card!!.extId.toInt()) + }, + ) as Map, (Chu3UserData, ExportOptions) -> Any?>, + customImporters = mapOf( + Chu3DataExport::userLoginBonusList to { export: Chu3DataExport, user: Chu3UserData -> + val userId = user.card!!.extId.toInt() + repos.userLoginBonus.deleteAll(repos.userLoginBonus.findByUser(userId)) + repos.userLoginBonus.saveAll(export.userLoginBonusList.map { it.apply { + id = 0 + this.user = userId + } }) + }, + ) as Map, (Chu3DataExport, Chu3UserData) -> Unit>, ) { override fun createEmpty() = Chu3DataExport() override val userDataRepo = repos.userData @@ -67,6 +84,16 @@ data class Chu3DataExport( var userMateList: List = emptyList(), var userGeneralDataList: List = emptyList(), var userMiscList: Chu3UserMisc = Chu3UserMisc(), + var userCardPrintStateList: List = emptyList(), + var userGachaList: List = emptyList(), + var userRegionsList: List = emptyList(), + var userCMissionList: List = emptyList(), + var userCMissionProgressList: List = emptyList(), + var netBattleLogList: List = emptyList(), + var userChallengeList: List = emptyList(), + var userLinkedVerseList: List = emptyList(), + var userVoteList: List = emptyList(), + var userLoginBonusList: List = emptyList(), ): IExportClass { constructor() : this("SDHD", Chu3UserData(), UserGameOption(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), Chu3UserMisc()) diff --git a/src/main/java/icu/samnyan/aqua/net/games/diva/DivaExport.kt b/src/main/java/icu/samnyan/aqua/net/games/diva/DivaExport.kt new file mode 100644 index 00000000..edce958e --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/games/diva/DivaExport.kt @@ -0,0 +1,33 @@ +package icu.samnyan.aqua.net.games.diva + +import ext.* +import icu.samnyan.aqua.net.db.AquaUserServices +import icu.samnyan.aqua.sega.diva.DivaRepos +import org.springframework.web.bind.annotation.RestController + +@RestController +@API("api/v2/game/diva") +class DivaExport( + val us: AquaUserServices, + val repos: DivaRepos, +) { + @API("export") + fun exportUserData(@RP token: Str) = us.jwt.auth(token) { account -> + val profile = repos.profile.findByPdId(account.ghostCard.extId).orElse(null) + ?: (404 - "User not found") + + linkedMapOf( + "gameId" to "SBZV", + "userData" to profile, + "gameSession" to repos.gameSession.findByPdId(profile).orElse(null), + "playLogList" to repos.playLog.findByPdId(profile), + "playerContestList" to repos.contest.findByPdId(profile), + "playerCustomizeList" to repos.customize.findByPdId(profile), + "playerInventoryList" to repos.inventory.findByPdId(profile), + "playerModuleList" to repos.module.findByPdId(profile), + "playerPvCustomizeList" to repos.pvCustomize.findByPdId(profile), + "playerPvRecordList" to repos.pvRecord.findByPdId(profile), + "playerScreenShotList" to repos.screenShot.findByPdId(profile), + ) + } +} diff --git a/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt b/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt index 850d0bb6..79c935a9 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt @@ -68,7 +68,7 @@ class Mai2Import( ?.split(",") ?.mapIndexed { index, id -> Mai2UserFavoriteItem().apply { orderId = index; this.id = id.toInt() } } ?: emptyList() - } + }, ) as Map, (Mai2UserDetail, ExportOptions) -> Any?>, customImporters = mapOf( Maimai2DataExport::userPlaylogList to { export: Maimai2DataExport, user: Mai2UserDetail -> @@ -85,7 +85,7 @@ class Mai2Import( propertyValue = favoriteMusicList.sortedBy { it.orderId }.map { it.id }.joinToString(",") }) } - } + }, ) as Map, (Maimai2DataExport, Mai2UserDetail) -> Unit> ) { override fun createEmpty() = Maimai2DataExport() @@ -113,6 +113,9 @@ data class Maimai2DataExport( var userFavoriteMusicList: List = mutableListOf(), var userKaleidxScopeList: List = mutableListOf(), var userPlaylogList: List = mutableListOf(), + var userCardList: List = mutableListOf(), + var userPrintDetailList: List = mutableListOf(), + var userRegionsList: List = mutableListOf(), // Not supported yet: // var userWeeklyData // var userMissionDataList diff --git a/src/main/java/icu/samnyan/aqua/net/games/wacca/Wacca.kt b/src/main/java/icu/samnyan/aqua/net/games/wacca/Wacca.kt index 8dad1db3..8c87c258 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/wacca/Wacca.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/wacca/Wacca.kt @@ -1,16 +1,10 @@ package icu.samnyan.aqua.net.games.wacca -import ext.API -import ext.RP -import ext.isoDate -import ext.utc +import ext.* import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.games.* import icu.samnyan.aqua.net.utils.waccaScores -import icu.samnyan.aqua.sega.wacca.model.db.WaccaUser -import icu.samnyan.aqua.sega.wacca.model.db.WcUserBestScoreRepo -import icu.samnyan.aqua.sega.wacca.model.db.WcUserPlayLogRepo -import icu.samnyan.aqua.sega.wacca.model.db.WcUserRepo +import icu.samnyan.aqua.sega.wacca.model.db.* import org.springframework.web.bind.annotation.RestController @RestController @@ -20,6 +14,7 @@ class Wacca( override val playlogRepo: WcUserPlayLogRepo, override val userDataRepo: WcUserRepo, override val userMusicRepo: WcUserBestScoreRepo, + val repos: WaccaRepos, ): GameApiController("wacca", WaccaUser::class) { override val settableFields: Map Unit> by lazy { mapOf( "userName" to usernameCheck(true), @@ -40,5 +35,38 @@ class Wacca( genericUserSummary(card, mapOf(), null, data?.favoriteSongs) } + @API("export") + fun exportUserData(@RP token: Str) = us.jwt.auth(token) { account -> + val user = repos.user.findByCard(account.ghostCard) ?: (404 - "User not found") + WaccaDataExport( + userData = user, + userOptionList = repos.option.findByUser(user), + userBingoList = repos.bingo.findByUser(user), + userFriendList = repos.friend.findByUser(user).map { + WaccaFriendExport(it.with.id, it.isAccepted) + }, + userGateList = repos.gate.findByUser(user), + userItemList = repos.item.findByUser(user), + userBestScoreList = repos.bestScore.findByUser(user), + userPlaylogList = repos.playLog.findByUser(user), + userStageUpList = repos.stageUp.findByUser(user), + ) + } + override val shownRanks: List> = waccaScores.filter { it.first > 85 * 10000 } } + +data class WaccaFriendExport(val withUserId: Long, val isAccepted: Boolean) + +data class WaccaDataExport( + val gameId: String = "SDFE", + val userData: WaccaUser, + val userOptionList: List, + val userBingoList: List, + val userFriendList: List, + val userGateList: List, + val userItemList: List, + val userBestScoreList: List, + val userPlaylogList: List, + val userStageUpList: List, +) diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt index 143e55d8..f53e014f 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/KeychipSession.kt @@ -5,7 +5,9 @@ import icu.samnyan.aqua.net.db.AquaNetUser import jakarta.persistence.* import jakarta.transaction.Transactional import org.slf4j.LoggerFactory -import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.JpaRepository +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Repository import org.springframework.stereotype.Service @@ -18,9 +20,10 @@ import java.security.SecureRandom @Table(name = "allnet_keychip_sessions", indexes = [ Index(name = "idx_last_use", columnList = "lastUse") ]) -class KeychipSession( - @ManyToOne - @JoinColumn(name = "au_id") +class KeychipSession( + @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "au_id") var user: AquaNetUser? = null, @Column(length = 4) @@ -45,12 +48,12 @@ fun genUrlSafeToken(length: Int): String { } @Repository("KeychipSessionRepo") -interface KeychipSessionRepo : JpaRepository { - fun findByToken(token: String): KeychipSession? - - @Transactional - fun deleteAllByLastUseBefore(expire: Long) -} +interface KeychipSessionRepo : JpaRepository { + fun findByToken(token: String): KeychipSession? + + @Transactional + fun deleteAllByLastUseBefore(expire: Long) +} @Service class KeychipSessionService( diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt index 49e70c80..5c73a69a 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt @@ -4,6 +4,8 @@ import icu.samnyan.aqua.net.db.AquaNetUser import jakarta.persistence.* import jakarta.transaction.Transactional import org.springframework.data.jpa.repository.JpaRepository +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import org.springframework.stereotype.Repository @Entity @@ -13,6 +15,7 @@ class UserKeychip( val id: Long = 0, @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "au_id", nullable = false) var user: AquaNetUser, diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt index ff318f75..cb95b51e 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt @@ -27,8 +27,10 @@ interface Chu3UserLinked : IUserRepo { // This repo cannot be generalized as UserLinked because the entity stores user as an int // TODO: Find a way to generalize this -interface Chu3UserLoginBonusRepo : JpaRepository { - @Query( +interface Chu3UserLoginBonusRepo : JpaRepository { + fun findByUser(userId: Int): List + + @Query( value = "select * from chusan_user_login_bonus where user = ?1 and version = ?2 and is_finished = ?3 order by last_update_date desc", nativeQuery = true ) diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserEntity.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserEntity.kt index a45722f1..b02c739c 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserEntity.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserEntity.kt @@ -6,11 +6,14 @@ import icu.samnyan.aqua.net.games.IUserEntity import jakarta.persistence.JoinColumn import jakarta.persistence.ManyToOne import jakarta.persistence.MappedSuperclass +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction @MappedSuperclass class Chu3UserEntity : BaseEntity(), IUserEntity { @JsonIgnore @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "user_id") override var user: Chu3UserData = Chu3UserData() -} \ No newline at end of file +} diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/DivaRepos.kt b/src/main/java/icu/samnyan/aqua/sega/diva/DivaRepos.kt index c1d19284..d22776a1 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/DivaRepos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/DivaRepos.kt @@ -67,6 +67,8 @@ interface PvEntryRepository : JpaRepository { } interface PlayerContestRepository : JpaRepository { + fun findByPdId(pdId: PlayerProfile): MutableList + fun findByPdIdAndContestId(pdId: PlayerProfile, contestId: Int): Optional fun findTop4ByPdIdOrderByLastUpdateTimeDesc(pdId: PlayerProfile): MutableList @@ -86,6 +88,8 @@ interface PlayerCustomizeRepository : JpaRepository { @Repository interface PlayerInventoryRepository : JpaRepository { + fun findByPdId(profile: PlayerProfile): MutableList + fun findByPdIdAndTypeAndValue(profile: PlayerProfile, type: String, value: String): Optional } @@ -96,6 +100,8 @@ interface PlayerProfileRepository : JpaRepository { @Repository interface PlayerPvCustomizeRepository : JpaRepository { + fun findByPdId(profile: PlayerProfile): MutableList + fun findByPdIdAndPvId(profile: PlayerProfile, pvId: Int): Optional } @@ -139,12 +145,16 @@ interface PlayerPvRecordRepository : JpaRepository { ): MutableList } -interface PlayerScreenShotRepository : JpaRepository +interface PlayerScreenShotRepository : JpaRepository { + fun findByPdId(profile: PlayerProfile): MutableList +} @Repository -interface PlayLogRepository : JpaRepository +interface PlayLogRepository : JpaRepository { + fun findByPdId(profile: PlayerProfile): MutableList +} @Repository interface PlayerModuleRepository : JpaRepository { fun findByPdId(profile: PlayerProfile): MutableList -} \ No newline at end of file +} diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/GameSession.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/GameSession.kt index c3fed1e5..d4a91924 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/GameSession.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/GameSession.kt @@ -2,6 +2,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata import icu.samnyan.aqua.sega.diva.model.common.StartMode import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.io.Serializable import java.time.LocalDateTime @@ -14,6 +16,7 @@ class GameSession : Serializable { var acceptId = 0 @OneToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id", unique = true) var pdId: PlayerProfile = PlayerProfile() diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayLog.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayLog.kt index 0c79f6c6..65692034 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayLog.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayLog.kt @@ -6,6 +6,8 @@ import icu.samnyan.aqua.sega.diva.model.common.ClearResult import icu.samnyan.aqua.sega.diva.model.common.Difficulty import icu.samnyan.aqua.sega.diva.model.common.Edition import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.io.Serializable import java.time.LocalDateTime @@ -17,6 +19,7 @@ class PlayLog : Serializable { var id: Long = 0 @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id") @JsonIgnore var pdId: PlayerProfile = PlayerProfile() diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerContest.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerContest.kt index ca00f302..0be1ad04 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerContest.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerContest.kt @@ -2,6 +2,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata import icu.samnyan.aqua.sega.diva.model.common.ContestBorder import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.io.Serializable import java.time.LocalDateTime @@ -13,6 +15,7 @@ class PlayerContest : Serializable { var id: Long = 0 @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id") var pdId: PlayerProfile = PlayerProfile() diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerCustomize.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerCustomize.kt index a59ae4d8..513dfd2f 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerCustomize.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerCustomize.kt @@ -1,6 +1,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.io.Serializable @Entity(name = "DivaPlayerCustomize") @@ -11,6 +13,7 @@ class PlayerCustomize : Serializable { var id: Long = 0 @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id") var pdId: PlayerProfile = PlayerProfile() diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerInventory.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerInventory.kt index 6d7e7901..13225bbf 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerInventory.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerInventory.kt @@ -2,6 +2,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata import com.fasterxml.jackson.annotation.JsonIgnore import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.io.Serializable @Entity(name = "DivaPlayerInventory") @@ -12,6 +14,7 @@ class PlayerInventory : Serializable { var id: Long = 0 @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id") @JsonIgnore var pdId: PlayerProfile = PlayerProfile() diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerModule.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerModule.kt index 9cc691bd..a7a98ad5 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerModule.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerModule.kt @@ -1,6 +1,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.io.Serializable @Entity(name = "DivaPlayerModule") @@ -11,6 +13,7 @@ class PlayerModule : Serializable { var id: Long = 0 @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id") var pdId: PlayerProfile = PlayerProfile() diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvCustomize.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvCustomize.kt index 3e5d276e..0f58cde0 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvCustomize.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvCustomize.kt @@ -2,6 +2,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata import com.fasterxml.jackson.annotation.JsonIgnore import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.io.Serializable @Entity(name = "DivaPlayerPvCustomize") @@ -12,6 +14,7 @@ class PlayerPvCustomize : Serializable { var id: Long = 0 @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id") @JsonIgnore var pdId: PlayerProfile = PlayerProfile() diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvRecord.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvRecord.kt index 64168803..ee0b428b 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvRecord.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerPvRecord.kt @@ -6,6 +6,8 @@ import icu.samnyan.aqua.sega.diva.model.common.ClearResult import icu.samnyan.aqua.sega.diva.model.common.Difficulty import icu.samnyan.aqua.sega.diva.model.common.Edition import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.io.Serializable @Entity(name = "DivaPlayerPvRecord") @@ -19,6 +21,7 @@ class PlayerPvRecord : Serializable { var id: Long = 0 @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id") @JsonIgnore var pdId: PlayerProfile = PlayerProfile() diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerScreenShot.kt b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerScreenShot.kt index 01d0caed..3235d3d5 100644 --- a/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerScreenShot.kt +++ b/src/main/java/icu/samnyan/aqua/sega/diva/model/db/userdata/PlayerScreenShot.kt @@ -1,6 +1,8 @@ package icu.samnyan.aqua.sega.diva.model.db.userdata import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction @Entity(name = "DivaPlayerScreenShot") @Table(name = "diva_player_screen_shot") @@ -10,6 +12,7 @@ class PlayerScreenShot { var id: Long = 0 @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "pd_id") var pdId: PlayerProfile = PlayerProfile() var pvId: Long = 0 diff --git a/src/main/java/icu/samnyan/aqua/sega/general/dao/CardRepository.kt b/src/main/java/icu/samnyan/aqua/sega/general/dao/CardRepository.kt index 538afe78..3307f061 100644 --- a/src/main/java/icu/samnyan/aqua/sega/general/dao/CardRepository.kt +++ b/src/main/java/icu/samnyan/aqua/sega/general/dao/CardRepository.kt @@ -11,4 +11,5 @@ import org.springframework.stereotype.Repository interface CardRepository : JpaRepository { fun findByExtId(extId: Long): Card? fun findByLuid(luid: String): Card? + fun findAllByAquaUserAuId(auId: Long): List } diff --git a/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt b/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt index fcd0183f..9c50ee08 100644 --- a/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt +++ b/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt @@ -3,6 +3,8 @@ package icu.samnyan.aqua.sega.general.model import com.fasterxml.jackson.annotation.JsonIgnore import icu.samnyan.aqua.net.db.AquaNetUser import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.time.LocalDateTime enum class CardStatus { @@ -59,6 +61,7 @@ class Card( // Defines the AquaNet user that this card is bound to @ManyToOne + @OnDelete(action = OnDeleteAction.SET_NULL) @JoinColumn(name = "net_user_id") @JsonIgnore var aquaUser: AquaNetUser? = null, diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt index 90f23f2c..326067a8 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt @@ -107,7 +107,7 @@ interface Mai2UserPlaylogRepo : GenericPlaylogRepo, Mai2UserLin fun findByUserAndUserPlayDateAfter(user: Mai2UserDetail, userPlayDate: String): List } -interface Mai2UserPrintDetailRepo : JpaRepository +interface Mai2UserPrintDetailRepo : Mai2UserLinked interface Mai2UserUdemaeRepo : Mai2UserLinked diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt index 4d39264c..1183c2cb 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt @@ -3,6 +3,7 @@ package icu.samnyan.aqua.sega.maimai2.model.userdata import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.JsonGenerator @@ -16,6 +17,8 @@ import icu.samnyan.aqua.net.games.IGenericUserMusic import icu.samnyan.aqua.net.games.IUserEntity import icu.samnyan.aqua.sega.general.IntegerListConverter import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -24,6 +27,7 @@ import java.time.format.DateTimeFormatter open class Mai2UserEntity : BaseEntity(), IUserEntity { @JsonIgnore @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "user_id") override var user: Mai2UserDetail = Mai2UserDetail() } @@ -459,6 +463,7 @@ fun main(args: Array) { @Table(name = "maimai2_user_print_detail") @Entity +@JsonIgnoreProperties(value = ["userCard"], allowSetters = true) class Mai2UserPrintDetail : Mai2UserEntity() { var orderId: Long = 0 var printNumber = 0 diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt index 47aa146d..d200a5f7 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt @@ -7,6 +7,8 @@ import icu.samnyan.aqua.net.games.* import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.util.AccessCodeSerializer import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.time.LocalDate import java.time.LocalDateTime @@ -14,6 +16,7 @@ import java.time.LocalDateTime class OngekiUserEntity : BaseEntity(), IUserEntity { @JsonIgnore @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "user_id") override var user: UserData = UserData() } @@ -541,4 +544,4 @@ class UserGacha : OngekiUserEntity() { var fiveGachaCnt = 0 var elevenGachaCnt = 0 var dailyGachaDate: String = LocalDateTime.now().toString() -} \ No newline at end of file +} diff --git a/src/main/java/icu/samnyan/aqua/sega/wacca/model/db/WaccaUserModels.kt b/src/main/java/icu/samnyan/aqua/sega/wacca/model/db/WaccaUserModels.kt index 4dc7af80..e26e3339 100644 --- a/src/main/java/icu/samnyan/aqua/sega/wacca/model/db/WaccaUserModels.kt +++ b/src/main/java/icu/samnyan/aqua/sega/wacca/model/db/WaccaUserModels.kt @@ -9,6 +9,8 @@ import icu.samnyan.aqua.sega.general.IntegerListConverter import icu.samnyan.aqua.sega.wacca.WaccaItemType import icu.samnyan.aqua.sega.wacca.WaccaItemType.* import jakarta.persistence.* +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction import java.util.* typealias UC = UniqueConstraint @@ -20,6 +22,7 @@ typealias UC = UniqueConstraint open class WaccaUserEntity : BaseEntity() { @JsonIgnore @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "user_id") open var user: WaccaUser = WaccaUser() } diff --git a/src/main/resources/db/120/V1000_83__diva_user_cascade.sql b/src/main/resources/db/120/V1000_83__diva_user_cascade.sql new file mode 100644 index 00000000..8ef1b894 --- /dev/null +++ b/src/main/resources/db/120/V1000_83__diva_user_cascade.sql @@ -0,0 +1,66 @@ +-- Diva's user foreign keys were removed in V1000.7 and never recreated. +-- Remove any rows that can no longer be associated with a profile before restoring them. +DELETE child FROM diva_game_session child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; +DELETE child FROM diva_play_log child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; +DELETE child FROM diva_player_contest child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; +DELETE child FROM diva_player_customize child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; +DELETE child FROM diva_player_inventory child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; +DELETE child FROM diva_player_module child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; +DELETE child FROM diva_player_pv_customize child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; +DELETE child FROM diva_player_pv_record child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; +DELETE child FROM diva_player_screen_shot child + LEFT JOIN diva_player_profile profile ON child.pd_id = profile.id + WHERE profile.id IS NULL; + +ALTER TABLE diva_game_session + ADD CONSTRAINT fku_diva_game_session_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE diva_play_log + ADD CONSTRAINT fku_diva_play_log_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE diva_player_contest + ADD CONSTRAINT fku_diva_player_contest_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE diva_player_customize + ADD CONSTRAINT fku_diva_player_customize_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE diva_player_inventory + ADD CONSTRAINT fku_diva_player_inventory_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE diva_player_module + ADD CONSTRAINT fku_diva_player_module_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE diva_player_pv_customize + ADD CONSTRAINT fku_diva_player_pv_customize_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE diva_player_pv_record + ADD CONSTRAINT fku_diva_player_pv_record_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE diva_player_screen_shot + ADD CONSTRAINT fku_diva_player_screen_shot_profile + FOREIGN KEY (pd_id) REFERENCES diva_player_profile (id) + ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/main/resources/db/120/V1000_84__account_owned_cascade.sql b/src/main/resources/db/120/V1000_84__account_owned_cascade.sql new file mode 100644 index 00000000..32add6df --- /dev/null +++ b/src/main/resources/db/120/V1000_84__account_owned_cascade.sql @@ -0,0 +1,42 @@ +-- Account-owned rows disappear with the account. Physical cards are only unlinked. +ALTER TABLE aqua_net_session + DROP FOREIGN KEY IF EXISTS FK_SESSION; +ALTER TABLE aqua_net_session + ADD CONSTRAINT FK_SESSION + FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id) + ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE aqua_net_email_confirmation + DROP FOREIGN KEY IF EXISTS FK_EMAIL_CONFIRMATION_ON_AQUA_USER; +ALTER TABLE aqua_net_email_confirmation + ADD CONSTRAINT FK_EMAIL_CONFIRMATION_ON_AQUA_USER + FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id) + ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE aqua_net_email_reset_password + DROP FOREIGN KEY IF EXISTS FK_EMAIL_RESET_PASSWORD_ON_AQUA_USER; +ALTER TABLE aqua_net_email_reset_password + ADD CONSTRAINT FK_EMAIL_RESET_PASSWORD_ON_AQUA_USER + FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id) + ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE allnet_keychip_sessions + DROP FOREIGN KEY IF EXISTS FK_ALLNET_KEYCHIP_SESSIONS_ON_AU; +ALTER TABLE allnet_keychip_sessions + ADD CONSTRAINT FK_ALLNET_KEYCHIP_SESSIONS_ON_AU + FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id) + ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE user_keychip + DROP FOREIGN KEY IF EXISTS fk_user_keychip_on_au; +ALTER TABLE user_keychip + ADD CONSTRAINT fk_user_keychip_on_au + FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id) + ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE sega_card + DROP FOREIGN KEY IF EXISTS FK_SEGA_CARD_ON_NET_USER; +ALTER TABLE sega_card + ADD CONSTRAINT FK_SEGA_CARD_ON_NET_USER + FOREIGN KEY (net_user_id) REFERENCES aqua_net_user (au_id) + ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/main/resources/db/120/V1000_85__ongeki_gacha_cascade.sql b/src/main/resources/db/120/V1000_85__ongeki_gacha_cascade.sql new file mode 100644 index 00000000..3d77ca56 --- /dev/null +++ b/src/main/resources/db/120/V1000_85__ongeki_gacha_cascade.sql @@ -0,0 +1,6 @@ +ALTER TABLE ongeki_user_gacha + DROP FOREIGN KEY IF EXISTS FK_ONGEKI_USER_GACHA_ON_USER; +ALTER TABLE ongeki_user_gacha + ADD CONSTRAINT FK_ONGEKI_USER_GACHA_ON_USER + FOREIGN KEY (user_id) REFERENCES ongeki_user_data (id) + ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/main/resources/db/120/V1000_86__maimai2_print_detail_cascade.sql b/src/main/resources/db/120/V1000_86__maimai2_print_detail_cascade.sql new file mode 100644 index 00000000..e89890f6 --- /dev/null +++ b/src/main/resources/db/120/V1000_86__maimai2_print_detail_cascade.sql @@ -0,0 +1,11 @@ +-- V1000.7 accidentally replaced the user foreign key with a second user-card +-- foreign key. Restore the direct ownership link so every print detail is +-- removed with its Mai2 user, including legacy rows without a user card. +DELETE detail FROM maimai2_user_print_detail detail + LEFT JOIN maimai2_user_detail user_data ON detail.user_id = user_data.id + WHERE user_data.id IS NULL; + +ALTER TABLE maimai2_user_print_detail + ADD CONSTRAINT fku_maimai2_user_print_detail_user + FOREIGN KEY (user_id) REFERENCES maimai2_user_detail (id) + ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/test/kotlin/test/Mai2UserPrintDetailJsonTest.kt b/src/test/kotlin/test/Mai2UserPrintDetailJsonTest.kt new file mode 100644 index 00000000..b08e258f --- /dev/null +++ b/src/test/kotlin/test/Mai2UserPrintDetailJsonTest.kt @@ -0,0 +1,23 @@ +package test + +import ext.JACKSON +import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserCard +import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class Mai2UserPrintDetailJsonTest : StringSpec({ + "userCard is accepted on input but omitted from output" { + val detail = Mai2UserPrintDetail().apply { + userCard = Mai2UserCard().apply { cardId = 42 } + } + + JACKSON.writeValueAsString(detail).contains("\"userCard\"") shouldBe false + + val imported = JACKSON.readValue( + """{"userCard":{"cardId":42}}""", + Mai2UserPrintDetail::class.java, + ) + imported.userCard?.cardId shouldBe 42 + } +})