From d2458633dba27dd72de3385f1c70ff8b8df65cff Mon Sep 17 00:00:00 2001 From: xds Date: Fri, 31 Oct 2025 17:11:40 +0300 Subject: [PATCH] tg login --- gradle.properties | 1 + .../luminic/finance/api/AuthController.kt | 61 ++++++++++++++++--- .../space/luminic/finance/dtos/UserDTO.kt | 16 ++++- .../luminic/finance/mappers/UserMapper.kt | 12 ++++ .../space/luminic/finance/models/User.kt | 3 +- .../space/luminic/finance/repos/UserRepo.kt | 4 +- .../luminic/finance/repos/UserRepoImpl.kt | 17 ++++-- .../luminic/finance/services/AuthService.kt | 33 +++++++--- .../luminic/finance/services/UserService.kt | 2 +- .../resources/application-prod.properties | 2 +- src/main/resources/db/migration/V22__.sql | 3 + 11 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 gradle.properties create mode 100644 src/main/resources/db/migration/V22__.sql diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/src/main/kotlin/space/luminic/finance/api/AuthController.kt b/src/main/kotlin/space/luminic/finance/api/AuthController.kt index 806d915..c4bac09 100644 --- a/src/main/kotlin/space/luminic/finance/api/AuthController.kt +++ b/src/main/kotlin/space/luminic/finance/api/AuthController.kt @@ -1,23 +1,67 @@ package space.luminic.finance.api +import org.apache.commons.codec.digest.DigestUtils.sha256 +import org.apache.commons.codec.digest.HmacUtils.hmacSha256 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.security.MessageDigest +import java.time.Instant +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec @RestController @RequestMapping("/auth") class AuthController( - private val authService: AuthService + private val authService: AuthService, + @Value("\${telegram.bot.token}") private val botToken: String ) { private val logger = LoggerFactory.getLogger(javaClass) + fun verifyTelegramAuth(data: Map, botToken: String): Boolean { + val hash = data["hash"] ?: return false + + val dataCheckString = data + .filterKeys { it != "hash" } + .toSortedMap() + .map { "${it.key}=${it.value}" } + .joinToString("\n") + + val secretKey = sha256(botToken) + val hmacHex = hmacSha256(secretKey, dataCheckString) + + if (hmacHex != hash) return false + + val authDate = data["auth_date"]?.toLongOrNull() ?: return false + val now = Instant.now().epochSecond + + // Опционально — запрет старых ответов (например, старше 1 часа) + val maxAgeSeconds = 3600 + if (now - authDate > maxAgeSeconds) return false + + return true + } + + 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 @@ -26,23 +70,26 @@ class AuthController( } @PostMapping("/login") - fun login(@RequestBody request: AuthUserDTO): Map { + 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 { + fun register(@RequestBody request: RegisterUserDTO): UserDTO { return authService.register(request.username, request.password, request.firstName).toDto() } - @PostMapping("/tgLogin") - fun tgLogin(@RequestHeader("X-Tg-Id") tgId: String): Map { - val token = authService.tgLogin(tgId) - return mapOf("token" to token) + @PostMapping("/tg-login") + fun tgLogin(@RequestBody tgUser: UserDTO.TelegramAuthDTO): String { + val ok = verifyTelegramAuth(tgUser.toTelegramMap(), botToken) + if (!ok) throw IllegalArgumentException("Invalid Telegram login") + return authService.tgAuth(tgUser) + } + @GetMapping("/me") fun getMe(): UserDTO { logger.info("Get Me") diff --git a/src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt index 34c19bb..de75793 100644 --- a/src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt +++ b/src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt @@ -1,11 +1,14 @@ package space.luminic.finance.dtos +import java.util.Date + data class UserDTO ( var id: Int, val username: String, var firstName: String, - var tgId: String? = null, + var tgId: Long? = null, var tgUserName: String? = null, + var photoUrl: String? = null, var roles: List ) { @@ -22,6 +25,17 @@ data class UserDTO ( ) + data class TelegramAuthDTO( + val id: Long, + val first_name: String?, + val last_name: String?, + val username: String?, + val photo_url: String?, + val auth_date: Long, + val hash: String + ) + + } diff --git a/src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt index b67dda6..4856ba8 100644 --- a/src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt +++ b/src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt @@ -1,6 +1,7 @@ package space.luminic.finance.mappers import space.luminic.finance.dtos.UserDTO +import space.luminic.finance.dtos.UserDTO.TelegramAuthDTO import space.luminic.finance.models.User object UserMapper { @@ -11,7 +12,18 @@ object UserMapper { firstName = this.firstName, tgId = this.tgId, tgUserName = this.tgUserName, + photoUrl = this.photoUrl, roles = this.roles ) + fun TelegramAuthDTO.toTelegramMap(): Map = + mapOf( + "id" to id.toString(), + "first_name" to (first_name ?: ""), + "last_name" to (last_name ?: ""), + "username" to (username ?: ""), + "photo_url" to (photo_url ?: ""), + "auth_date" to auth_date.toString(), + "hash" to hash + ) } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/User.kt b/src/main/kotlin/space/luminic/finance/models/User.kt index bc1941a..8a92984 100644 --- a/src/main/kotlin/space/luminic/finance/models/User.kt +++ b/src/main/kotlin/space/luminic/finance/models/User.kt @@ -11,8 +11,9 @@ data class User( var id: Int? = null, val username: String, var firstName: String, - var tgId: String? = null, + var tgId: Long? = null, var tgUserName: String? = null, + val photoUrl: String? = null, var password: String? = null, var isActive: Boolean = true, var regDate: LocalDate = LocalDate.now(), diff --git a/src/main/kotlin/space/luminic/finance/repos/UserRepo.kt b/src/main/kotlin/space/luminic/finance/repos/UserRepo.kt index b1ea9db..7af7c1f 100644 --- a/src/main/kotlin/space/luminic/finance/repos/UserRepo.kt +++ b/src/main/kotlin/space/luminic/finance/repos/UserRepo.kt @@ -9,8 +9,8 @@ interface UserRepo { fun findById(id: Int): User? fun findByUsername(username: String): User? fun findParticipantsBySpace(spaceId: Int): Set - fun findByTgId(tgId: String): User? - fun save(user: User): User + fun findByTgId(tgId: Long): User? + fun create(user: User): User fun update(user: User): User fun deleteById(id: Long) } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/repos/UserRepoImpl.kt b/src/main/kotlin/space/luminic/finance/repos/UserRepoImpl.kt index 89832ec..ba558b4 100644 --- a/src/main/kotlin/space/luminic/finance/repos/UserRepoImpl.kt +++ b/src/main/kotlin/space/luminic/finance/repos/UserRepoImpl.kt @@ -4,18 +4,20 @@ import org.springframework.jdbc.core.RowMapper import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate import org.springframework.stereotype.Repository import space.luminic.finance.models.User + @Repository class UserRepoImpl( private val jdbcTemplate: NamedParameterJdbcTemplate -) : UserRepo{ +) : UserRepo { private fun userRowMapper() = RowMapper { rs, _ -> User( id = rs.getInt("id"), username = rs.getString("username"), firstName = rs.getString("first_name"), - tgId = rs.getString("tg_id"), + tgId = rs.getLong("tg_id"), tgUserName = rs.getString("tg_user_name"), + photoUrl = rs.getString("photo_url"), password = rs.getString("password"), isActive = rs.getBoolean("is_active"), regDate = rs.getDate("reg_date").toLocalDate(), @@ -41,11 +43,12 @@ class UserRepoImpl( } override fun findParticipantsBySpace(spaceId: Int): Set { - val sql = "select * from finance.users u join finance.spaces_participants sp on sp.participants_id = u.id where sp.space_id = :spaceId" + val sql = + "select * from finance.users u join finance.spaces_participants sp on sp.participants_id = u.id where sp.space_id = :spaceId" return jdbcTemplate.query(sql, mapOf("spaceId" to spaceId), userRowMapper()).toSet() } - override fun findByTgId(tgId: String): User? { + override fun findByTgId(tgId: Long): User? { val sql = """ select * from finance.users u where tg_id = :tgId """.trimIndent() @@ -53,13 +56,15 @@ class UserRepoImpl( return jdbcTemplate.queryForObject(sql, params, userRowMapper()) } - override fun save(user: User): User { - val sql = "insert into finance.users(username, first_name, tg_id, tg_user_name, password, is_active, reg_date) values (:username, :firstname, :tg_id, :tg_user_name, :password, :isActive, :regDate) returning ID" + override fun create(user: User): User { + val sql = + "insert into finance.users(username, first_name, tg_id, tg_user_name, photo_url, password, is_active, reg_date) values (:username, :firstname, :tg_id, :tg_user_name, :photo_url :password, :isActive, :regDate) returning ID" val params = mapOf( "username" to user.username, "firstname" to user.firstName, "tg_id" to user.tgId, "tg_user_name" to user.tgUserName, + "photo_url" to user.photoUrl, "password" to user.password, "isActive" to user.isActive, "regDate" to user.regDate, diff --git a/src/main/kotlin/space/luminic/finance/services/AuthService.kt b/src/main/kotlin/space/luminic/finance/services/AuthService.kt index f2f9490..5cdcfa5 100644 --- a/src/main/kotlin/space/luminic/finance/services/AuthService.kt +++ b/src/main/kotlin/space/luminic/finance/services/AuthService.kt @@ -6,6 +6,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.stereotype.Service import space.luminic.finance.configs.AuthException +import space.luminic.finance.dtos.UserDTO +import space.luminic.finance.models.NotFoundException import space.luminic.finance.models.Token import space.luminic.finance.models.User import space.luminic.finance.repos.UserRepo @@ -44,7 +46,7 @@ class AuthService( return username.toInt() } - fun login(username: String, password: String): String { + fun login(username: String, password: String): String { val user = userRepo.findByUsername(username) ?: throw UsernameNotFoundException("Пользователь не найден") return if (passwordEncoder.matches(password, user.password)) { @@ -61,10 +63,12 @@ class AuthService( } } - fun tgLogin(tgId: String): String { - val user = - userRepo.findByTgId(tgId) ?: throw UsernameNotFoundException("Пользователь не найден") - + fun tgAuth(tgUser: UserDTO.TelegramAuthDTO): String { + val user: User = try { + tgLogin(tgUser.id) + } catch (e: NotFoundException) { + registerTg(tgUser) + } val token = jwtUtil.generateToken(user.username) val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10) tokenService.saveToken( @@ -73,7 +77,22 @@ class AuthService( expiresAt = expireAt.toInstant() ) return token + } + fun registerTg(tgUser: UserDTO.TelegramAuthDTO): User { + val user = User( + username = tgUser.username ?: UUID.randomUUID().toString().split('-')[0], + firstName = tgUser.first_name ?: UUID.randomUUID().toString().split('-')[0], + tgId = tgUser.id, + tgUserName = tgUser.username, + photoUrl = tgUser.photo_url, + roles = mutableListOf("USER") + ) + return userRepo.create(user) + } + + fun tgLogin(tgId: Long): User { + return userRepo.findByTgId(tgId) ?: throw NotFoundException("User with provided TG id $tgId not found") } fun register(username: String, password: String, firstName: String): User { @@ -85,14 +104,14 @@ class AuthService( firstName = firstName, roles = mutableListOf("USER") ) - newUser = userRepo.save(newUser) + newUser = userRepo.create(newUser) return newUser } else throw IllegalArgumentException("Пользователь уже зарегистрирован") } @Cacheable(cacheNames = ["tokens"], key = "#token") - fun isTokenValid(token: String): User { + fun isTokenValid(token: String): User { val tokenDetails = tokenService.getToken(token) when { tokenDetails.status == Token.TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(Instant.now()) -> { diff --git a/src/main/kotlin/space/luminic/finance/services/UserService.kt b/src/main/kotlin/space/luminic/finance/services/UserService.kt index b86ffb7..8abf154 100644 --- a/src/main/kotlin/space/luminic/finance/services/UserService.kt +++ b/src/main/kotlin/space/luminic/finance/services/UserService.kt @@ -24,7 +24,7 @@ class UserService(val userRepo: UserRepo) { } fun getUserByTelegramId(telegramId: Long): User { - return userRepo.findByTgId(telegramId.toString())?: throw NotFoundException("User with telegramId: $telegramId not found") + return userRepo.findByTgId(telegramId)?: throw NotFoundException("User with telegramId: $telegramId not found") } diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 2800d5d..36df05f 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -13,7 +13,7 @@ logging.level.org.mongodb.driver.protocol.command = INFO #management.endpoint.metrics.access=read_only -telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY +telegram.bot.token = 7999296388:AAGXPE5r0yt3ZFehBoUh8FGm5FBbs9pYIks nlp.address=https://nlp.luminic.space diff --git a/src/main/resources/db/migration/V22__.sql b/src/main/resources/db/migration/V22__.sql new file mode 100644 index 0000000..6595205 --- /dev/null +++ b/src/main/resources/db/migration/V22__.sql @@ -0,0 +1,3 @@ +alter table finance.users + add column photo_url varchar null, + alter column tg_id set data type numeric; \ No newline at end of file