diff --git a/AquaNet/src/libs/generalTypes.ts b/AquaNet/src/libs/generalTypes.ts index 77c1831b..069b8e91 100644 --- a/AquaNet/src/libs/generalTypes.ts +++ b/AquaNet/src/libs/generalTypes.ts @@ -31,6 +31,7 @@ export interface AquaNetUser { computedName: string, password: string, optOutOfLeaderboard: boolean, + canModifyKeychips: boolean, } export interface CardSummaryGame { diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index 3dec9139..8bdd4cb4 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -142,7 +142,7 @@ export const EN_REF_HOME = { export const EN_REF_SETUP = { 'setup.welcome': `Welcome! If you have a game set up, please follow the instructions below to set up the connection with AquaDX.`, - 'setup.keychip-warning': `Your keychip is linked to your account and should be kept secure. Do not give others your keychip.`, + 'setup.keychip-warning': `Your keychip(s) are linked to your account and should be kept secure.`, 'setup.steps.one': `Pick a method of setting up network communications. Some browsers may not be able to do automatic setup.`, 'setup.steps.two': `Link your Aime card to your AquaDX account using the Cards page via it's access code or serial number.`, 'setup.steps.three': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`, @@ -164,6 +164,11 @@ export const EN_REF_SETUP = { (instructions for Card Maker)`, 'setup.troubleshooting.items.three': `"I can't scan my card!"
Emulated card readers, by default, are configured to use the ENTER key to scan in (hold it).` + 'setup.keychip': 'Keychip Management', + 'setup.keychip.warning': 'Keychips can be manually set here for cabinet owners with physical keychips trying to connect to AquaDX. Selecting a keychip will autofill it for the setup below.', + 'setup.keychip.add': `Add keychip`, + 'setup.keychip.delete': `Delete`, + 'setup.keychip.placeholder': `New Keychip ID` } export const EN_REF_SETTINGS = { diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index ce4ad8ce..1c04775c 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -186,8 +186,12 @@ export const USER = { ensureLoggedIn() return post('/api/v2/user/me', {}) }, - keychip: (): Promise => - post('/api/v2/user/keychip', {}).then(it => it.keychip), + keychips: (): Promise => + post('/api/v2/user/keychip', {}).then(it => it.keychips), + addKeychip: (keychipId: string): Promise => + post('/api/v2/user/keychip/add', { keychipId }).then(it => it.keychipId), + deleteKeychip: (keychipId: string) => + post('/api/v2/user/keychip/delete', { keychipId }), setting: (key: string, value: string) => post('/api/v2/user/setting', { key: key === 'password' ? 'pwHash' : key, value }), uploadPfp: (file: File) => { diff --git a/AquaNet/src/pages/Home/SetupInstructions.svelte b/AquaNet/src/pages/Home/SetupInstructions.svelte index 12149331..6f5ba7f0 100644 --- a/AquaNet/src/pages/Home/SetupInstructions.svelte +++ b/AquaNet/src/pages/Home/SetupInstructions.svelte @@ -11,34 +11,100 @@ import { patchUserSegatools } from "../../libs/setup"; let user: AquaNetUser - let keychip: string; + let keychips: string[] = []; + let selectedKeychip: string = ""; let keychipCode: string; let exposeKeychip = false; let automaticSetupStatus: "none" | "success" | "failure" = "none"; + let isLoading = true; + let isAdding = false; + let newKeychip = ""; + let addKeychipError = ""; - USER.me().then((u) => { - user = u; - USER.keychip().then(k => { - keychip = `${k.slice(0, 4)}-${k.slice(4)}1337`; - codeToHtml(` + function formatKeychipDisplay(k: string): string { + return `${k.slice(0, 4)}-${k.slice(4)}`; + } + + function buildManualKeychipLines(): string { + return `id=${formatKeychipDisplay(selectedKeychip)}`; + } + + async function buildKeychipCode() { + exposeKeychip = false; + const keychipLines = buildManualKeychipLines(); + keychipCode = await codeToHtml(` [dns] default=${AQUA_CONNECTION} [keychip] enable=1 -id=${keychip}`.trim(), { - lang: 'ini', - theme: 'rose-pine', - transformers: [] - }).then((html) => { - keychipCode = html; - }); +${keychipLines}`.trim(), { + lang: 'ini', + theme: 'rose-pine', + transformers: [] }); + } + + async function loadKeychips() { + isLoading = true; + keychips = await USER.keychips(); + if (keychips.length > 0) { + selectedKeychip = keychips[0]; + await buildKeychipCode(); + } else { + selectedKeychip = ""; + await buildKeychipCode(); + } + isLoading = false; + } + + USER.me().then((u) => { + user = u; + loadKeychips(); }); + async function selectKeychip(k: string) { + selectedKeychip = k; + await buildKeychipCode(); + } + + async function addKeychip() { + const rawKeychipId = newKeychip.trim().toUpperCase(); + const validRawFormat = /^A\d{14}$/.test(rawKeychipId) || /^A\d{3}-\d{11}$/.test(rawKeychipId); + if (!validRawFormat) { + addKeychipError = "Invalid keychip format; use A12345678901234 or A123-12345678901."; + return; + } + + const keychipId = rawKeychipId.replace("-", ""); + + addKeychipError = ""; + isAdding = true; + try { + const newId = await USER.addKeychip(keychipId); + keychips = [...keychips, newId]; + selectedKeychip = newId; + newKeychip = ""; + await buildKeychipCode(); + } catch (error) { + addKeychipError = error instanceof Error ? error.message : "Failed to add keychip."; + } finally { + isAdding = false; + } + } + + async function deleteKeychip(k: string) { + await USER.deleteKeychip(k); + keychips = keychips.filter(id => id !== k); + if (selectedKeychip === k) { + selectedKeychip = keychips[0] ?? ""; + await buildKeychipCode(); + } + } + async function patchSegatools() { - automaticSetupStatus = await patchUserSegatools({ keychip, dns: AQUA_CONNECTION }) ? "success" : "failure"; + automaticSetupStatus = await patchUserSegatools({ keychip: formatKeychipDisplay(selectedKeychip), dns: AQUA_CONNECTION }) ? "success" : "failure"; } @@ -47,45 +113,87 @@ id=${keychip}`.trim(), {

{t('home.setup')}

- {#if keychip} + {#if isLoading} +

{t('loading')}

+ {:else}
1.
{@html t('setup.steps.one')}
- +
{t('setup.keychip-warning')}
- - {#if !!window.showOpenFilePicker} + + {#if user.canModifyKeychips}
- {t('setup.type.automatic')} - {@html t('setup.automatic')} - {#if automaticSetupStatus != "none"} -
- {t(`setup.automatic.${automaticSetupStatus}`)} -
- {/if} -
- + {t('setup.keychip')} +

+ {t('setup.keychip.warning')} +

+
+ {#each keychips as k} +
+ + +
+ {/each} + +
+ + +
+ {#if addKeychipError} +

{addKeychipError}

+ {/if}
+
{/if} -
- {t('setup.type.manual')} - {@html t('setup.manual')} -
-
- {@html keychipCode} + {#if selectedKeychip} + {#if !!window.showOpenFilePicker} +
+ {t('setup.type.automatic')} + {@html t('setup.automatic')} + {#if automaticSetupStatus != "none"} +
+ {t(`setup.automatic.${automaticSetupStatus}`)} +
+ {/if} +
+ +
+
+ {/if} + +
+ {t('setup.type.manual')} + {@html t('setup.manual')} +
+
+ {@html keychipCode} +
+ {#if !exposeKeychip} + + {/if}
- {#if !exposeKeychip} - - {/if} -
-
-
+ +
+ {/if}
2.
{@html t('setup.steps.two')}
@@ -107,9 +215,7 @@ id=${keychip}`.trim(), {

{@html t('setup.support-info')} -

- {:else} -

{t('loading')}

+

{/if}
@@ -122,6 +228,17 @@ id=${keychip}`.trim(), { ul li margin: 0.75em 0 + .divider + width: 90% + height: 1px + + background: #fff3 + + margin: 1em 0 + + position: relative + left: 50% + transform: translate(-50%, 0) :global(pre.shiki) background-color: transparent !important @@ -179,5 +296,48 @@ id=${keychip}`.trim(), { top: 50% left: 50% transform: translate(-50%, -50%) - + + .keychip-list + display: flex + flex-direction: column + gap: 0.5em + margin: 1em 0 + + .keychip-item + display: flex + align-items: center + gap: 0.5em + padding: 0.25em 0.5em + border-radius: 4px + &.selected + background: vars.$c-shadow + + .keychip-select + font-family: monospace + flex: 1 + text-align: left + + .add-keychip + align-self: flex-start + margin-top: 0.25em + + .add-keychip-form + display: flex + flex-wrap: wrap + gap: 0.5em + align-items: center + margin-top: 0.25em + + input + flex: 1 1 16rem + max-width: 16rem + + button + flex: 1 1 10rem + max-width: 8rem + + .danger + color: vars.$c-error + + diff --git a/config/application.properties b/config/application.properties index ddbbdfc6..7b63286f 100644 --- a/config/application.properties +++ b/config/application.properties @@ -97,6 +97,7 @@ spring.datasource.hikari.maximum-pool-size=10 ## Link card limit aqua-net.link-card-limit=10 +aqua-net.keychip-limit=20 ## CloudFlare Turnstile Captcha ## This enables captcha for user registration. diff --git a/src/main/java/icu/samnyan/aqua/net/Bot.kt b/src/main/java/icu/samnyan/aqua/net/Bot.kt index b6d76c06..1d86051b 100644 --- a/src/main/java/icu/samnyan/aqua/net/Bot.kt +++ b/src/main/java/icu/samnyan/aqua/net/Bot.kt @@ -116,7 +116,7 @@ class BotController( val gamesDict = listOfNotNull(chu3, mai2).map { // Find the keychip owner val keychip = it.lastClientId - val keychipOwner = keychip?.let { us.userRepo.findByKeychip(it) } + val keychipOwner = keychip?.let { k -> us.userKeychipRepo.findByKeychipId(k)?.user } mapOf( "userData" to it, diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index 99915057..57ecf165 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -2,12 +2,16 @@ 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.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 @@ -33,6 +37,8 @@ class UserRegistrar( val cardRepo: CardRepository, val validator: AquaUserServices, val emailProps: EmailProperties, + val userKeychipRepo: UserKeychipRepo, + val aquaNetProps: AquaNetProps, final val paths: PathProps ) { val portraitPath = paths.aquaNetPortrait.path() @@ -235,20 +241,57 @@ class UserRegistrar( SUCCESS } + val keychipPattern = Regex("^A\\d{14}$") + val dashedKeychipPattern = Regex("^A\\d{3}-\\d{11}$") val keychipRange = 1e9.toULong()..1e10.toULong() - 1UL + + private fun ensureCanModifyKeychips(u: AquaNetUser) { + if (!u.canModifyKeychips) 403 - "You don't have permission to modify keychips" + } + + private fun validateCustomKeychip(keychipId: Str): Str { + val raw = keychipId.trim().uppercase() + val normalized = raw.replace("-", "") + + val validRawFormat = raw == normalized || dashedKeychipPattern.matches(raw) + if (!validRawFormat || !keychipPattern.matches(normalized)) + 400 - "Invalid keychip format. Expected A followed by 14 digits (with optional dash)" + + return normalized + } + @API("/keychip") - @Doc("Get a Keychip ID so that the user can connect to the server.", "Success message") - suspend fun setupConnection(@RP token: Str) = jwt.auth(token) { u -> - u.keychip?.let { return mapOf("keychip" to it) } - log.info("Net: /user/keychip setup: ${u.auId} for ${u.username}") + @Doc("List all keychip IDs associated with the current user's account.", "List of keychip IDs") + suspend fun listKeychips(@RP token: Str) = jwt.auth(token) { u -> + val keychips = async { userKeychipRepo.findAllByUserAuId(u.auId) } + mapOf("keychips" to keychips.map { it.keychipId }) + } - // Generate a keychip id with 10 digits (e.g. A1234567890) - var new = "A" + keychipRange.random() - while (async { userRepo.findByKeychip(new) != null }) new = "A" + keychipRange.random() - async { userRepo.save(u.apply { keychip = new }) } + @API("/keychip/add") + @Doc("Add a custom keychip ID for the user.", "The newly added keychip ID") + suspend fun addKeychip(@RP token: Str, @RP keychipId: Str) = jwt.auth(token) { u -> + ensureCanModifyKeychips(u) + log.info("Net: /user/keychip/add: ${u.auId} for ${u.username}") + val validated = validateCustomKeychip(keychipId) - mapOf("keychip" to new) + if (async { userKeychipRepo.existsByKeychipId(validated) }) + 400 - "Keychip already exists" + + if (userKeychipRepo.findAllByUserAuId(u.auId).size >= aquaNetProps.keychipLimit) + 400 - "Exceeds maximum keychip count" + + async { userKeychipRepo.save(UserKeychip(user = u, keychipId = validated)) } + mapOf("keychipId" to validated) + } + + @API("/keychip/delete") + @Doc("Remove a keychip ID from the user's account.", "Success message") + suspend fun deleteKeychip(@RP token: Str, @RP keychipId: Str) = jwt.auth(token) { u -> + ensureCanModifyKeychips(u) + val deleted = async { userKeychipRepo.deleteByKeychipIdAndUserAuId(keychipId, u.auId) } + if (deleted == 0L) 404 - "Keychip not found" + SUCCESS } @API("/upload-pfp", consumes = ["multipart/form-data"]) 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 34b2221b..811a9883 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -6,8 +6,9 @@ import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdEnd import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdStart import icu.samnyan.aqua.net.components.JWT import icu.samnyan.aqua.sega.allnet.AllNetProps -import icu.samnyan.aqua.sega.allnet.KeyChipRepo import icu.samnyan.aqua.sega.allnet.KeychipSession +import icu.samnyan.aqua.sega.allnet.UserKeychip +import icu.samnyan.aqua.sega.allnet.UserKeychipRepo import icu.samnyan.aqua.sega.general.GameMusicPopularity import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.model.Card @@ -74,16 +75,19 @@ class AquaNetUser( @OneToMany(mappedBy = "aquaUser", cascade = [CascadeType.ALL]) var cards: MutableList = mutableListOf(), - // Each user can have one keychip (if the user owns a cabinet) + // Each user can have multiple keychips (if the user owns cabinets) @JsonIgnore - @Column(nullable = true, length = 32, unique = true) - var keychip: Str? = null, + @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL]) + var keychips: MutableList = mutableListOf(), // Each user's keychip can have multiple sessions @JsonIgnore @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL]) var keychipSessions: MutableList = mutableListOf(), + @Column(nullable = false) + var canModifyKeychips: Boolean = false, + @OneToOne(cascade = [CascadeType.ALL]) @JoinColumn(name = "gameOptions", unique = true, nullable = true) var gameOptions: AquaGameOptions? = null, @@ -105,7 +109,6 @@ interface AquaNetUserRepo : JpaRepository { fun findByAuId(auId: Long): AquaNetUser? fun findByEmailIgnoreCase(email: String): AquaNetUser? fun findByUsernameIgnoreCase(username: String): AquaNetUser? - fun findByKeychip(keychip: String): AquaNetUser? fun findByGhostCardExtId(extId: Long): AquaNetUser? } @@ -124,7 +127,7 @@ class AquaUserServices( val userRepo: AquaNetUserRepo, val cardRepo: CardRepository, val hasher: PasswordEncoder, - val keyChipRepo: KeyChipRepo, + val userKeychipRepo: UserKeychipRepo, val allNetProps: AllNetProps, val jwt: JWT, val em: EntityManager, @@ -142,9 +145,18 @@ class AquaUserServices( } } + val keychipRange = 1e9.toULong()..1e10.toULong() - 1UL + private fun generateKeychipId(): String { + // 1337 is retained for backwards compatibility, it's no longer required for cabinet keychips + var keychip = "A" + keychipRange.random() + "1337" + while ( userKeychipRepo.existsByKeychipId(keychip) ) + keychip = "A" + keychipRange.random() + "1337" + return keychip + } + fun create(username: Str, email: Str, password: Str, country: Str, emailConfirmed: Boolean = false): AquaNetUser { // Create user - val u = AquaNetUser( + val user = AquaNetUser( username = checkUsername(username), email = validateEmail(email), pwHash = checkPwHash(password), @@ -158,16 +170,20 @@ class AquaUserServices( luid = extId.toString() registerTime = LocalDateTime.now() accessTime = registerTime - aquaUser = u + aquaUser = user isGhost = true } - u.ghostCard = card + user.ghostCard = card + + // Create an automatic keychip + val keychip = UserKeychip(0, user, generateKeychipId()) // Save the user - userRepo.save(u) + userRepo.save(user) cardRepo.save(card) + userKeychipRepo.save(keychip) - return u + return user } fun update(user: AquaNetUser, key: Str, value: Str) { @@ -192,7 +208,7 @@ class AquaUserServices( fun validKeychip(keychipId: Str): Bool { if (!allNetProps.checkKeychip) return true if (keychipId.isBlank()) return false - if (userRepo.findByKeychip(keychipId) != null || keyChipRepo.existsByKeychipId(keychipId)) return true + if (userKeychipRepo.existsByKeychipId(keychipId)) return true return false } diff --git a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt index 100e7175..0b5d0f56 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt @@ -4,6 +4,7 @@ import ext.* import icu.samnyan.aqua.net.BotProps import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.utils.SUCCESS +import icu.samnyan.aqua.sega.allnet.UserKeychipRepo import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.service.CardService import jakarta.annotation.PostConstruct @@ -32,6 +33,7 @@ abstract class GameApiController(val name: String, userDataClass: open val gettableFields: Set = setOf() @Autowired lateinit var cardService: CardService + @Autowired lateinit var userKeychipRepo: UserKeychipRepo @API("trend") abstract suspend fun trend(@RP username: String): List @@ -205,7 +207,7 @@ abstract class GameApiController(val name: String, userDataClass: lastVersion = user.lastRomVersion, ratingComposition = ratingComp, recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(100).reversed(), - lastPlayedHost = user.lastClientId?.let { us.userRepo.findByKeychip(it)?.username }, + lastPlayedHost = user.lastClientId?.let { userKeychipRepo.findByKeychipId(it)?.user?.username }, rival = rival, favorites = favorites ) diff --git a/src/main/java/icu/samnyan/aqua/net/utils/AquaNetProps.kt b/src/main/java/icu/samnyan/aqua/net/utils/AquaNetProps.kt index 6843da21..3fe18bb4 100644 --- a/src/main/java/icu/samnyan/aqua/net/utils/AquaNetProps.kt +++ b/src/main/java/icu/samnyan/aqua/net/utils/AquaNetProps.kt @@ -10,6 +10,7 @@ import kotlin.io.path.createDirectories @ConfigurationProperties(prefix = "aqua-net") class AquaNetProps { var linkCardLimit: Int = 10 + var keychipLimit: Int = 20 var importBackupPath = "data/import-backups" @PostConstruct diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt index 9fa44e84..bc89868f 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt @@ -1,7 +1,6 @@ package icu.samnyan.aqua.sega.allnet import ext.* -import icu.samnyan.aqua.net.db.AquaNetUserRepo import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder.decodeAllNet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -59,9 +58,8 @@ class AllNetProps { @Suppress("HttpUrlsUsage") @RestController class AllNet( - val userRepo: AquaNetUserRepo, + val userKeychipRepo: UserKeychipRepo, val keychipSessionService: KeychipSessionService, - val keychipRepo: KeyChipRepo, val props: AllNetProps ) { @API("/") @@ -109,7 +107,7 @@ class AllNet( // Proper keychip authentication if (props.checkKeychip) { // If it's a user keychip, it should be in user database - val u = userRepo.findByKeychip(serial) + val u = findUserByIncomingSerial(serial) if (u != null) { // Create a new session for the user logger.info("> Keychip authenticated: ${u.auId} ${u.computedName}") @@ -120,11 +118,6 @@ class AllNet( session = keychipSessionService.new(u, reqMap["game_id"] ?: "").token } - // Check if it's a whitelisted keychip - else if (!serial.isEmpty() && keychipRepo.existsByKeychipId(serial)) { - session = keychipSessionService.new(null, reqMap["game_id"] ?: "").token - } - else if (props.keychipPermissiveForTesting) { logger.warn("> Accepted invalid keychip $serial in permissive mode") session = keychipSessionService.new(null, reqMap["game_id"] ?: "").token @@ -175,6 +168,17 @@ class AllNet( return resp.toUrl() + "\n" } + private fun findUserByIncomingSerial(serial: String) = when (serial.length) { + FULL_KEYCHIP_LENGTH -> userKeychipRepo.findByKeychipId(serial)?.user + SHORT_KEYCHIP_LENGTH -> { + // segatools only sends the first 11 characters of the keychip + // First, try to find it by suffixing AquaDX's generated suffix, then fall back to matching without the suffixed 4 digits + userKeychipRepo.findByKeychipId(serial + KEYCHIP_SUFFIX)?.user + ?: userKeychipRepo.findByKeychipIdStartingWith(serial)?.user + } + else -> userKeychipRepo.findByKeychipId(serial)?.user + } + private fun switchUri(hereAddr: Str, localPort: Str, gameId: Str, ver: Str, session: Str?): Str { val addr = hereAddr + (if (props.hidePort) "" else ":${props.port ?: localPort}") @@ -185,6 +189,9 @@ class AllNet( } companion object { + const val SHORT_KEYCHIP_LENGTH = 11 + const val FULL_KEYCHIP_LENGTH = 15 + const val KEYCHIP_SUFFIX = "1337" val logger = logger() } } diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNetSecure.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNetSecure.kt index 21f420f1..f7d69848 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNetSecure.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNetSecure.kt @@ -25,7 +25,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Component @ConditionalOnBean(AllNetSecureInit::class) class TokenChecker( - val keyChipRepo: KeyChipRepo, val keychipSessionService: KeychipSessionService, val frontierProps: FrontierProps, val geoip: GeoIP @@ -56,7 +55,7 @@ class TokenChecker( // The token can either be a keychip id (old method) or a session id (new method) // Or the frontier token val session = keychipSessionService.find(token) - if (token.isNotBlank() && (keyChipRepo.existsByKeychipId(token) || session != null + if (token.isNotBlank() && (session != null || (frontierProps.enabled && frontierProps.ftk == token))) { currentSession.set(session) diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/Keychip.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/Keychip.kt deleted file mode 100644 index f88ff798..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/Keychip.kt +++ /dev/null @@ -1,32 +0,0 @@ -package icu.samnyan.aqua.sega.allnet - -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.Id -import jakarta.persistence.Table -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( - @Id - val id: Long = 0, - - @Column(unique = true, nullable = false) - val keychipId: String = "", -) : Serializable { - companion object { - const val serialVersionUID = 1L - } -} - -@Repository("KeyChipRepository") -interface KeyChipRepo : JpaRepository { - fun existsByKeychipId(keychipId: String?): Boolean -} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt new file mode 100644 index 00000000..49e70c80 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt @@ -0,0 +1,32 @@ +package icu.samnyan.aqua.sega.allnet + +import icu.samnyan.aqua.net.db.AquaNetUser +import jakarta.persistence.* +import jakarta.transaction.Transactional +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Entity +@Table(name = "user_keychip") +class UserKeychip( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @ManyToOne + @JoinColumn(name = "au_id", nullable = false) + var user: AquaNetUser, + + @Column(unique = true, nullable = false, length = 32) + val keychipId: String, +) + +@Repository +interface UserKeychipRepo : JpaRepository { + fun findByKeychipId(keychipId: String): UserKeychip? + fun findByKeychipIdStartingWith(keychipIdPrefix: String): UserKeychip? + fun existsByKeychipId(keychipId: String): Boolean + fun findAllByUserAuId(auId: Long): List + + @Transactional + fun deleteByKeychipIdAndUserAuId(keychipId: String, auId: Long): Long +} diff --git a/src/main/resources/db/80/V1000_72__user_keychip.sql b/src/main/resources/db/80/V1000_72__user_keychip.sql new file mode 100644 index 00000000..0819f874 --- /dev/null +++ b/src/main/resources/db/80/V1000_72__user_keychip.sql @@ -0,0 +1,27 @@ +-- Create the user_keychip join table +CREATE TABLE user_keychip +( + id BIGINT AUTO_INCREMENT NOT NULL, + au_id BIGINT NOT NULL, + keychip_id VARCHAR(32) NOT NULL, + CONSTRAINT pk_user_keychip PRIMARY KEY (id) +); + +ALTER TABLE user_keychip + ADD CONSTRAINT uc_user_keychip_keychip_id UNIQUE (keychip_id); + +ALTER TABLE user_keychip + ADD CONSTRAINT fk_user_keychip_on_au FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id) ON DELETE CASCADE; + +-- Migrate existing keychip values from aqua_net_user into the new table +INSERT INTO user_keychip (au_id, keychip_id) +SELECT au_id, keychip +FROM aqua_net_user +WHERE keychip IS NOT NULL; + +-- Drop the old keychip column +ALTER TABLE aqua_net_user + DROP CONSTRAINT uc_aqua_net_user_keychip; + +ALTER TABLE aqua_net_user + DROP COLUMN keychip; diff --git a/src/main/resources/db/80/V1000_73__keychip_ids_to_15_digits.sql b/src/main/resources/db/80/V1000_73__keychip_ids_to_15_digits.sql new file mode 100644 index 00000000..d8c2f97c --- /dev/null +++ b/src/main/resources/db/80/V1000_73__keychip_ids_to_15_digits.sql @@ -0,0 +1,5 @@ +-- Normalize stored keychip IDs to 15 digits by appending our suffix. +-- Existing IDs are historically 11 characters (A + 10 digits). +UPDATE user_keychip +SET keychip_id = CONCAT(keychip_id, '1337') +WHERE CHAR_LENGTH(keychip_id) = 11; diff --git a/src/main/resources/db/80/V1000_74__user_can_modify_keychips.sql b/src/main/resources/db/80/V1000_74__user_can_modify_keychips.sql new file mode 100644 index 00000000..e9750f05 --- /dev/null +++ b/src/main/resources/db/80/V1000_74__user_can_modify_keychips.sql @@ -0,0 +1,2 @@ +ALTER TABLE aqua_net_user + ADD COLUMN can_modify_keychips BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/src/main/resources/db/80/V1000_75__drop_keychip_whitelist.sql b/src/main/resources/db/80/V1000_75__drop_keychip_whitelist.sql new file mode 100644 index 00000000..36203b2e --- /dev/null +++ b/src/main/resources/db/80/V1000_75__drop_keychip_whitelist.sql @@ -0,0 +1 @@ +DROP TABLE allnet_keychips; \ No newline at end of file