mirror of
https://github.com/hykilpikonna/AquaDX.git
synced 2026-05-06 21:12:38 -05:00
Merge branch 'v1-dev' into encryption
This commit is contained in:
commit
edff6e83c5
|
|
@ -31,6 +31,7 @@ export interface AquaNetUser {
|
|||
computedName: string,
|
||||
password: string,
|
||||
optOutOfLeaderboard: boolean,
|
||||
canModifyKeychips: boolean,
|
||||
}
|
||||
|
||||
export interface CardSummaryGame {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
32
src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt
Normal file
32
src/main/java/icu/samnyan/aqua/sega/allnet/UserKeychip.kt
Normal 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
|
||||
}
|
||||
27
src/main/resources/db/80/V1000_72__user_keychip.sql
Normal file
27
src/main/resources/db/80/V1000_72__user_keychip.sql
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE aqua_net_user
|
||||
ADD COLUMN can_modify_keychips BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE allnet_keychips;
|
||||
Loading…
Reference in New Issue
Block a user