AquaDX/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt

169 lines
6.1 KiB
Kotlin

package icu.samnyan.aqua.net
import ext.*
import icu.samnyan.aqua.net.components.*
import icu.samnyan.aqua.net.db.*
import icu.samnyan.aqua.net.db.AquaUserServices.Companion.SETTING_FIELDS
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
import jakarta.servlet.http.HttpServletRequest
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.RestController
import java.time.Instant
import java.time.LocalDateTime
@RestController
@API("/api/v2/user")
class UserRegistrar(
val userRepo: AquaNetUserRepo,
val hasher: PasswordEncoder,
val turnstileService: TurnstileService,
val emailService: EmailService,
val geoIP: GeoIP,
val jwt: JWT,
val confirmationRepo: EmailConfirmationRepo,
val cardRepo: CardRepository,
val cardService: CardService,
val validator: AquaUserServices,
val emailProps: EmailProperties
) {
companion object {
// Random long with length 9-10
// We chose 1e9 as the start because normal cards took 0...1e9-1
// This is because games can only take uint32 for card ID, which is at max 10 digits (4294967295)
const val cardExtIdStart = 1e9.toLong()
const val cardExtIdEnd = 4294967295
}
@API("/register")
@Doc("Register a new user. This will also create a ghost card for the user and send a confirmation email.", "Success message")
suspend fun register(
@RP username: Str, @RP email: Str, @RP password: Str, @RP turnstile: Str,
request: HttpServletRequest
): Any {
val ip = geoIP.getIP(request)
// Check captcha
if (!turnstileService.validate(turnstile, ip)) 400 - "Invalid captcha"
// 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
isGhost = true
}
u.ghostCard = card
// Save the user
async {
userRepo.save(u)
cardRepo.save(card)
}
// Send confirmation email
emailService.sendConfirmation(u)
return mapOf("success" to true)
}
@API("/login")
@Doc("Login with email/username and password. This will also check if the email is verified and send another confirmation", "JWT token")
suspend fun login(
@RP email: Str, @RP password: Str, @RP turnstile: Str,
request: HttpServletRequest
): Any {
// Check captcha
val ip = geoIP.getIP(request)
if (!turnstileService.validate(turnstile, ip)) 400 - "Invalid captcha"
// Treat email as email / username
val user = async { userRepo.findByEmailIgnoreCase(email) ?: userRepo.findByUsernameIgnoreCase(email) }
?: (400 - "User not found")
if (!hasher.matches(password, user.pwHash)) 400 - "Invalid password"
// Check if email is verified
if (!user.emailConfirmed && emailProps.enable) {
// Check if last confirmation email was sent within a minute
val confirmations = async { confirmationRepo.findByAquaNetUserAuId(user.auId) }
val lastConfirmation = confirmations.maxByOrNull { it.createdAt }
if (lastConfirmation?.createdAt?.plusSeconds(60)?.isAfter(Instant.now()) == true) {
400 - "Email not verified - STATE_0"
}
// Check if we have sent more than 3 confirmation emails in the last 24 hours
if (confirmations.count { it.createdAt.plusSeconds(60 * 60 * 24).isAfter(Instant.now()) } > 3) {
400 - "Email not verified - STATE_1"
}
// Send another confirmation email
emailService.sendConfirmation(user)
400 - "Email not verified - STATE_2"
}
// Generate JWT token
val token = jwt.gen(user)
// Set last login time
async { userRepo.save(user.apply { lastLogin = millis() }) }
return mapOf("token" to token)
}
@API("/confirm-email")
@Doc("Confirm email address with a token sent through email to the user.", "Success message")
suspend fun confirmEmail(@RP token: Str): Any {
// Find the confirmation
val confirmation = async { confirmationRepo.findByToken(token) }
// Check if the token is valid
if (confirmation == null) 400 - "Invalid token"
// Check if the token is expired
if (confirmation.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired"
// Confirm the email
async { userRepo.save(confirmation.aquaNetUser.apply { emailConfirmed = true }) }
return SUCCESS
}
@API("/me")
@Doc("Get the information of the current logged-in user.", "User information")
suspend fun getUser(@RP token: Str) = jwt.auth(token)
@API("/setting")
@Doc("Validate and set a user setting field.", "Success message")
suspend fun setting(@RP token: Str, @RP key: Str, @RP value: Str) = jwt.auth(token) { u ->
// Check if the key is a settable field
val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")
async {
// Set the validated field
field.setter.call(u, field.checker.call(validator, value))
// Save the user
userRepo.save(u)
}
SUCCESS
}
}