package space.luminic.finance.api 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 import org.springframework.web.bind.annotation.* import space.luminic.finance.dtos.UserDTO import space.luminic.finance.dtos.UserDTO.AuthUserDTO 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 import javax.crypto.spec.SecretKeySpec @RestController @RequestMapping("/auth") class AuthController( private val authService: AuthService, @Value("\${telegram.bot.token}") private val botToken: String ) { private val logger = LoggerFactory.getLogger(javaClass) fun verifyTelegramAuth( loginData: Map? = null, // from login widget webAppInitData: String? = null ): Boolean { // --- 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 = MessageDigest.getInstance("SHA-256") .digest(botToken.toByteArray()) val hmac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(secretKey, "HmacSHA256")) }.doFinal(dataCheckString.toByteArray()) .joinToString("") { "%02x".format(it) } val authDate = loginData["auth_date"]?.toLongOrNull() ?: return false if (Instant.now().epochSecond - authDate > 3600) return false if (hmac == hash) return true } } // --- WEBAPP CHECK --- // --- WEBAPP CHECK --- if (webAppInitData != null) { // Разбираем query string корректно (учитывая '=' внутри значения) val pairs: Map = 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 = MessageDigest.getInstance("SHA-256").digest(input.toByteArray()) private fun hmacSha256(secret: ByteArray, message: String): String { val key = SecretKeySpec(secret, "HmacSHA256") val mac = Mac.getInstance("HmacSHA256") mac.init(key) val hashBytes = mac.doFinal(message.toByteArray()) return hashBytes.joinToString("") { "%02x".format(it) } } @GetMapping("/test") fun test(): String { val authentication = SecurityContextHolder.getContext().authentication logger.info("SecurityContext in controller: $authentication") return "Hello, ${authentication.name}" } @PostMapping("/login") fun login(@RequestBody request: AuthUserDTO): Map { val token = authService.login(request.username.lowercase(), request.password) return mapOf("token" to token) } @PostMapping("/register") fun register(@RequestBody request: RegisterUserDTO): UserDTO { 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 { // println(tgUser.hash) // println(botToken) 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(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") authService.getSecurityUser() return authService.getSecurityUser().toDto() } }