From c88a98e355a2615da38667b5a3465c5e185ad2e4 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:47:17 -0500 Subject: [PATCH] [+] Separate user validator --- .../icu/samnyan/aqua/net/UserRegistrar.kt | 47 ++++------- .../icu/samnyan/aqua/net/db/AquaNetUser.kt | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index dfa2b1e3..08d4b949 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -7,7 +7,10 @@ import icu.samnyan.aqua.net.components.JWT import icu.samnyan.aqua.net.components.TurnstileService import icu.samnyan.aqua.net.db.AquaNetUser import icu.samnyan.aqua.net.db.AquaNetUserRepo +import icu.samnyan.aqua.net.db.AquaUserValidator +import icu.samnyan.aqua.net.db.AquaUserValidator.Companion.SETTING_FIELDS import icu.samnyan.aqua.net.db.EmailConfirmationRepo +import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.service.CardService @@ -19,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController import java.time.Instant import java.time.LocalDateTime import java.util.Random +import kotlin.reflect.KMutableProperty @RestController @API("/api/v2/user") @@ -31,7 +35,8 @@ class UserRegistrar( val jwt: JWT, val confirmationRepo: EmailConfirmationRepo, val cardRepo: CardRepository, - val cardService: CardService + val cardService: CardService, + val validator: AquaUserValidator, ) { companion object { // Random long with length 19 (10^19 possibilities) @@ -53,46 +58,26 @@ class UserRegistrar( // Check captcha if (!turnstileService.validate(turnstile, ip)) 400 - "Invalid captcha" - // Check if email is valid - if (!email.isValidEmail()) 400 - "Invalid email" - - // Check if user with the same email exists - if (async { userRepo.findByEmailIgnoreCase(email) != null }) - 400 - "User with email `$email` already exists" - - // Check if username is valid - if (username.length < 2) 400 - "Username must be at least 2 letters" - if (username.length > 32) 400 - "Username too long (max 32 letters)" - if (username.contains(" ")) 400 - "Username cannot contain spaces" - - // Check if username is within A-Za-z0-9_-~. - username.find { !it.isLetterOrDigit() && it != '_' && it != '-' && it != '~' && it != '.' }?.let { - 400 - "Username cannot contain `$it`. Please only use letters (A-Z), numbers (0-9), and `_-~.` characters. You can set a display name later." - } - - // Check if user with the same username exists - if (async { userRepo.findByUsernameIgnoreCase(username) != null }) - 400 - "User with username `$username` already exists" - - // Validate password - if (password.length < 8) 400 - "Password must be at least 8 characters" - // GeoIP check to infer country val country = geoIP.getCountry(ip) + // Create user + val u = async { AquaNetUser( + username = validator.checkUsername(username), + email = validator.checkEmail(email), + pwHash = validator.checkPwHash(password), + regTime = millis(), lastLogin = millis(), country = country, + ) } + // Create a ghost card val card = Card().apply { extId = cardService.randExtID(cardExtIdStart, cardExtIdEnd) luid = extId.toString() registerTime = LocalDateTime.now() accessTime = registerTime + aquaUser = u } - val u = AquaNetUser( - username = username, email = email, pwHash = hasher.encode(password), - regTime = millis(), lastLogin = millis(), country = country, - ghostCard = card - ) - card.aquaUser = u + u.ghostCard = card // Save the user async { 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 a1df7675..cfe2a60c 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -1,10 +1,18 @@ package icu.samnyan.aqua.net.db +import ext.Str +import ext.isValidEmail +import ext.minus import icu.samnyan.aqua.sega.general.model.Card import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Repository +import org.springframework.stereotype.Service import java.io.Serializable +import kotlin.reflect.KFunction +import kotlin.reflect.KMutableProperty +import kotlin.reflect.full.functions @Entity(name = "AquaNetUser") @Table(name = "aqua_net_user") @@ -57,4 +65,77 @@ interface AquaNetUserRepo : JpaRepository { fun findByAuId(auId: Long): AquaNetUser? fun findByEmailIgnoreCase(email: String): AquaNetUser? fun findByUsernameIgnoreCase(username: String): AquaNetUser? +} + +data class SettingField( + val name: Str, + val checker: KFunction<*>, + val setter: KMutableProperty.Setter<*>, +) + +/** + * This class is a validator for user fields. It will return the parsed value if the field is valid, or + * throw an ApiException if the field is invalid. + */ +@Service +class AquaUserValidator( + val userRepo: AquaNetUserRepo, + val hasher: PasswordEncoder, +) { + companion object { + val SETTING_FIELDS = AquaUserValidator::class.functions + .filter { it.name.startsWith("check") } + .map { + val name = it.name.removePrefix("check").replaceFirstChar { c -> c.lowercase() } + val prop = AquaNetUser::class.members.find { m -> m.name == name } as KMutableProperty<*> + SettingField(name, it, prop.setter) + } + } + + fun checkUsername(username: Str) = username.apply { + // Check if username is valid + if (length < 2) 400 - "Username must be at least 2 letters" + if (length > 32) 400 - "Username too long (max 32 letters)" + if (contains(" ")) 400 - "Username cannot contain spaces" + + // Check if username is within A-Za-z0-9_-~. + find { !it.isLetterOrDigit() && it != '_' && it != '-' && it != '~' && it != '.' }?.let { + 400 - "Username cannot contain `$it`. Please only use letters (A-Z), numbers (0-9), and `_-~.` characters. You can set a display name later." + } + + // Check if user with the same username exists + if (userRepo.findByUsernameIgnoreCase(this) != null) + 400 - "User with username `$this` already exists" + } + + fun checkEmail(email: Str) = email.apply { + // Check if email is valid + if (!isValidEmail()) 400 - "Invalid email" + + // Check if user with the same email exists + if (userRepo.findByEmailIgnoreCase(email) != null) + 400 - "User with email `$email` already exists" + } + + fun checkPwHash(password: Str) = password.run { + // Validate password + if (length < 8) 400 - "Password must be at least 8 characters" + + hasher.encode(this) + } + + fun checkDisplayName(displayName: Str) = displayName.apply { + // Check if display name is valid + if (length > 32) 400 - "Display name too long (max 32 letters)" + } + + fun checkProfileLocation(profileLocation: Str) = profileLocation.apply { + // Check if profile location is valid + if (length > 64) 400 - "Profile location too long (max 64 letters)" + } + + fun checkProfileBio(profileBio: Str) = profileBio.apply { + // Check if profile bio is valid + if (length > 255) 400 - "Profile bio too long (max 255 letters)" + } } \ No newline at end of file