recurrents

This commit is contained in:
xds
2025-11-17 15:02:47 +03:00
parent d0cae182b7
commit 12afd1f90e
48 changed files with 1479 additions and 120 deletions

View File

@@ -1,8 +1,8 @@
package space.luminic.finance.api
import org.apache.commons.codec.digest.DigestUtils.sha256
import org.apache.commons.codec.digest.HmacUtils.hmacSha256
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonBuilder
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.core.context.SecurityContextHolder
@@ -13,6 +13,7 @@ import space.luminic.finance.dtos.UserDTO.RegisterUserDTO
import space.luminic.finance.mappers.UserMapper.toDto
import space.luminic.finance.mappers.UserMapper.toTelegramMap
import space.luminic.finance.services.AuthService
import java.net.URLDecoder
import java.security.MessageDigest
import java.time.Instant
import javax.crypto.Mac
@@ -27,28 +28,83 @@ class AuthController(
private val logger = LoggerFactory.getLogger(javaClass)
fun verifyTelegramAuth(data: Map<String, String>, botToken: String): Boolean {
val hash = data["hash"] ?: return false
fun verifyTelegramAuth(
loginData: Map<String, String>? = null, // from login widget
webAppInitData: String? = null
): Boolean {
val dataCheckString = data
.filterKeys { it != "hash" }
.toSortedMap()
.map { "${it.key}=${it.value}" }
.joinToString("\n")
// --- LOGIN WIDGET CHECK ---
if (loginData != null) {
val hash = loginData["hash"]
if (hash != null) {
val dataCheckString = loginData
.filterKeys { it != "hash" }
.toSortedMap()
.map { "${it.key}=${it.value}" }
.joinToString("\n")
val secretKey = sha256(botToken)
val hmacHex = hmacSha256(secretKey, dataCheckString)
val secretKey = MessageDigest.getInstance("SHA-256")
.digest(botToken.toByteArray())
if (hmacHex != hash) return false
val hmac = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec(secretKey, "HmacSHA256"))
}.doFinal(dataCheckString.toByteArray())
.joinToString("") { "%02x".format(it) }
val authDate = data["auth_date"]?.toLongOrNull() ?: return false
val now = Instant.now().epochSecond
val authDate = loginData["auth_date"]?.toLongOrNull() ?: return false
if (Instant.now().epochSecond - authDate > 3600) return false
// Опционально — запрет старых ответов (например, старше 1 часа)
val maxAgeSeconds = 3600
if (now - authDate > maxAgeSeconds) return false
if (hmac == hash) return true
}
}
return true
// --- WEBAPP CHECK ---
// --- WEBAPP CHECK ---
if (webAppInitData != null) {
// Разбираем query string корректно (учитывая '=' внутри значения)
val pairs: Map<String, String> = webAppInitData.split("&")
.mapNotNull { part ->
val idx = part.indexOf('=')
if (idx <= 0) return@mapNotNull null
val k = part.substring(0, idx)
val v = part.substring(idx + 1)
k to URLDecoder.decode(v, Charsets.UTF_8.name())
}.toMap()
val receivedHash = pairs["hash"] ?: return false
// Строка для подписи: все поля КРОМЕ hash, отсортированные по ключу, в формате key=value с \n
val dataCheckString = pairs
.filterKeys { it != "hash" }
.toSortedMap()
.entries
.joinToString("\n") { (k, v) -> "$k=$v" }
// ВАЖНО: secret_key = HMAC_SHA256(message=botToken, key="WebAppData")
val secretKeyBytes = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec("WebAppData".toByteArray(Charsets.UTF_8), "HmacSHA256"))
}.doFinal(botToken.toByteArray(Charsets.UTF_8))
// hash = HMAC_SHA256(message=data_check_string, key=secret_key)
val calcHashHex = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec(secretKeyBytes, "HmacSHA256"))
}.doFinal(dataCheckString.toByteArray(Charsets.UTF_8))
.joinToString("") { "%02x".format(it) }
// опциональная проверка свежести
val authDate = pairs["auth_date"]?.toLongOrNull() ?: return false
val now = Instant.now().epochSecond
val ttl = 6 * 3600L // например, 6 часов для WebApp
val skew = 300L // допускаем до 5 минут будущего/прошлого
val diff = now - authDate
if (diff > ttl || diff < -skew) return false
if (calcHashHex == receivedHash) return true
}
return false
return false
}
private fun sha256(input: String): ByteArray =
@@ -62,6 +118,7 @@ class AuthController(
return hashBytes.joinToString("") { "%02x".format(it) }
}
@GetMapping("/test")
fun test(): String {
val authentication = SecurityContextHolder.getContext().authentication
@@ -80,18 +137,45 @@ class AuthController(
return authService.register(request.username, request.password, request.firstName).toDto()
}
private val json = Json { ignoreUnknownKeys = true }
@PostMapping("/tg-login")
fun tgLogin(@RequestBody tgUser: UserDTO.TelegramAuthDTO): Map<String, String> {
// println(tgUser.hash)
// println(botToken)
val ok = verifyTelegramAuth(tgUser.toTelegramMap(), botToken)
if (!ok) throw IllegalArgumentException("Invalid Telegram login")
return mapOf("token" to authService.tgAuth(tgUser))
if (tgUser.initData == null) {
if (verifyTelegramAuth(
loginData = tgUser.toTelegramMap(),
)
) {
return mapOf("token" to authService.tgAuth(tgUser))
} else throw IllegalArgumentException("Invalid Telegram login")
} else {
if (verifyTelegramAuth(webAppInitData = tgUser.initData)) {
val params = tgUser.initData.split("&").associate {
val (k, v) = it.split("=", limit = 2)
k to URLDecoder.decode(v, "UTF-8")
}
val userJson = params["user"] ?: error("No user data")
val jsonUser = json.decodeFromString<UserDTO.TelegramUserData>(userJson)
val newUser = UserDTO.TelegramAuthDTO(
jsonUser.id,
jsonUser.first_name,
jsonUser.last_name,
jsonUser.username,
jsonUser.photo_url,
null,
hash = tgUser.hash,
initData = null,
)
return mapOf("token" to authService.tgAuth(newUser))
} else throw IllegalArgumentException("Invalid Telegram login")
}
}
@GetMapping("/me")
fun getMe(): UserDTO {
logger.info("Get Me")