Merge branch 'v1-dev' into encryption

This commit is contained in:
Raymond 2026-04-13 14:29:24 -04:00 committed by GitHub
commit edff6e83c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 388 additions and 114 deletions

View File

@ -31,6 +31,7 @@ export interface AquaNetUser {
computedName: string,
password: string,
optOutOfLeaderboard: boolean,
canModifyKeychips: boolean,
}
export interface CardSummaryGame {

View File

@ -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 <a href="/cards">Cards</a> 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 = {
<a target="_blank" href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/SDED#patching-assemblycsharpdll">(instructions for Card Maker)</a>`,
'setup.troubleshooting.items.three': `"I can't scan my card!"<br>
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 = {

View File

@ -186,8 +186,12 @@ export const USER = {
ensureLoggedIn()
return post('/api/v2/user/me', {})
},
keychip: (): Promise<string> =>
post('/api/v2/user/keychip', {}).then(it => it.keychip),
keychips: (): Promise<string[]> =>
post('/api/v2/user/keychip', {}).then(it => it.keychips),
addKeychip: (keychipId: string): Promise<string> =>
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) => {

View File

@ -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";
}
</script>
@ -47,45 +113,87 @@ id=${keychip}`.trim(), {
<div class="setup-instructions">
<h2>{t('home.setup')}</h2>
{#if keychip}
{#if isLoading}
<p>{t('loading')}</p>
{:else}
<div class="setup-step">
1. <div>{@html t('setup.steps.one')}</div>
</div>
<blockquote class="info">
{t('setup.keychip-warning')}
</blockquote>
{#if !!window.showOpenFilePicker}
{#if user.canModifyKeychips}
<details>
<summary>{t('setup.type.automatic')}</summary>
{@html t('setup.automatic')}
{#if automaticSetupStatus != "none"}
<blockquote class={`keychip-status ${automaticSetupStatus}`}>
{t(`setup.automatic.${automaticSetupStatus}`)}
</blockquote>
{/if}
<div class="setup-btn">
<button on:click={patchSegatools}>{t('setup.automatic.select')}</button>
<summary>{t('setup.keychip')}</summary>
<p>
{t('setup.keychip.warning')}
</p>
<div class="keychip-list">
{#each keychips as k}
<div class="keychip-item" class:selected={k === selectedKeychip}>
<button class="keychip-select" on:click={() => selectKeychip(k)}>
{formatKeychipDisplay(k)}
</button>
<button class="keychip-delete danger" on:click={() => deleteKeychip(k)}>
{t('setup.keychip.delete')}
</button>
</div>
{/each}
<form class="add-keychip-form" on:submit|preventDefault={addKeychip}>
<input
type="text"
placeholder={t('setup.keychip.placeholder')}
maxlength="16"
bind:value={newKeychip}
required
/>
<button class="add-keychip" type="submit" disabled={isAdding}>
{isAdding ? t('loading') : t('setup.keychip.add')}
</button>
</form>
{#if addKeychipError}
<p class="danger">{addKeychipError}</p>
{/if}
</div>
</details>
<div class="divider"></div>
{/if}
<details>
<summary>{t('setup.type.manual')}</summary>
{@html t('setup.manual')}
<div class="code-container">
<div class="code" class:revealed={exposeKeychip}>
{@html keychipCode}
{#if selectedKeychip}
{#if !!window.showOpenFilePicker}
<details>
<summary>{t('setup.type.automatic')}</summary>
{@html t('setup.automatic')}
{#if automaticSetupStatus != "none"}
<blockquote class={`keychip-status ${automaticSetupStatus}`}>
{t(`setup.automatic.${automaticSetupStatus}`)}
</blockquote>
{/if}
<div class="setup-btn">
<button on:click={patchSegatools}>{t('setup.automatic.select')}</button>
</div>
</details>
{/if}
<details>
<summary>{t('setup.type.manual')}</summary>
{@html t('setup.manual')}
<div class="code-container">
<div class="code" class:revealed={exposeKeychip}>
{@html keychipCode}
</div>
{#if !exposeKeychip}
<button class="reveal-btn" on:click={() => exposeKeychip = true}>
{t('setup.reveal-keychip')}
</button>
{/if}
</div>
{#if !exposeKeychip}
<button class="reveal-btn" on:click={() => exposeKeychip = true}>
{t('setup.reveal-keychip')}
</button>
{/if}
</div>
</details>
<br>
</details>
<br>
{/if}
<div class="setup-step">
2. <div>{@html t('setup.steps.two')}</div>
@ -107,9 +215,7 @@ id=${keychip}`.trim(), {
</ul>
<p>
{@html t('setup.support-info')}
</p>
{:else}
<p>{t('loading')}</p>
</p>
{/if}
</div>
</main>
@ -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
</style>

View File

@ -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.

View File

@ -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,

View File

@ -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"])

View File

@ -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<Card> = 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<UserKeychip> = mutableListOf(),
// Each user's keychip can have multiple sessions
@JsonIgnore
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL])
var keychipSessions: MutableList<KeychipSession> = 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<AquaNetUser, Long> {
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
}

View File

@ -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<T : IUserData>(val name: String, userDataClass:
open val gettableFields: Set<String> = setOf()
@Autowired lateinit var cardService: CardService
@Autowired lateinit var userKeychipRepo: UserKeychipRepo
@API("trend")
abstract suspend fun trend(@RP username: String): List<TrendOut>
@ -205,7 +207,7 @@ abstract class GameApiController<T : IUserData>(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
)

View File

@ -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

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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<Keychip?, Long?> {
fun existsByKeychipId(keychipId: String?): Boolean
}

View File

@ -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<UserKeychip, Long> {
fun findByKeychipId(keychipId: String): UserKeychip?
fun findByKeychipIdStartingWith(keychipIdPrefix: String): UserKeychip?
fun existsByKeychipId(keychipId: String): Boolean
fun findAllByUserAuId(auId: Long): List<UserKeychip>
@Transactional
fun deleteByKeychipIdAndUserAuId(keychipId: String, auId: Long): Long
}

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,2 @@
ALTER TABLE aqua_net_user
ADD COLUMN can_modify_keychips BOOLEAN NOT NULL DEFAULT FALSE;

View File

@ -0,0 +1 @@
DROP TABLE allnet_keychips;