This commit is contained in:
xds
2025-10-31 17:11:40 +03:00
parent 5b9d2366db
commit d2458633db
11 changed files with 128 additions and 26 deletions

1
gradle.properties Normal file
View File

@@ -0,0 +1 @@
kotlin.code.style=official

View File

@@ -1,23 +1,67 @@
package space.luminic.finance.api 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.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import space.luminic.finance.dtos.UserDTO import space.luminic.finance.dtos.UserDTO
import space.luminic.finance.dtos.UserDTO.AuthUserDTO import space.luminic.finance.dtos.UserDTO.AuthUserDTO
import space.luminic.finance.dtos.UserDTO.RegisterUserDTO import space.luminic.finance.dtos.UserDTO.RegisterUserDTO
import space.luminic.finance.mappers.UserMapper.toDto import space.luminic.finance.mappers.UserMapper.toDto
import space.luminic.finance.mappers.UserMapper.toTelegramMap
import space.luminic.finance.services.AuthService import space.luminic.finance.services.AuthService
import java.security.MessageDigest
import java.time.Instant
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
class AuthController( class AuthController(
private val authService: AuthService private val authService: AuthService,
@Value("\${telegram.bot.token}") private val botToken: String
) { ) {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
fun verifyTelegramAuth(data: Map<String, String>, 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") @GetMapping("/test")
fun test(): String { fun test(): String {
val authentication = SecurityContextHolder.getContext().authentication val authentication = SecurityContextHolder.getContext().authentication
@@ -36,13 +80,16 @@ class AuthController(
return authService.register(request.username, request.password, request.firstName).toDto() return authService.register(request.username, request.password, request.firstName).toDto()
} }
@PostMapping("/tgLogin") @PostMapping("/tg-login")
fun tgLogin(@RequestHeader("X-Tg-Id") tgId: String): Map<String, String> { fun tgLogin(@RequestBody tgUser: UserDTO.TelegramAuthDTO): String {
val token = authService.tgLogin(tgId) val ok = verifyTelegramAuth(tgUser.toTelegramMap(), botToken)
return mapOf("token" to token) if (!ok) throw IllegalArgumentException("Invalid Telegram login")
return authService.tgAuth(tgUser)
} }
@GetMapping("/me") @GetMapping("/me")
fun getMe(): UserDTO { fun getMe(): UserDTO {
logger.info("Get Me") logger.info("Get Me")

View File

@@ -1,11 +1,14 @@
package space.luminic.finance.dtos package space.luminic.finance.dtos
import java.util.Date
data class UserDTO ( data class UserDTO (
var id: Int, var id: Int,
val username: String, val username: String,
var firstName: String, var firstName: String,
var tgId: String? = null, var tgId: Long? = null,
var tgUserName: String? = null, var tgUserName: String? = null,
var photoUrl: String? = null,
var roles: List<String> var roles: List<String>
) { ) {
@@ -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
)
} }

View File

@@ -1,6 +1,7 @@
package space.luminic.finance.mappers package space.luminic.finance.mappers
import space.luminic.finance.dtos.UserDTO import space.luminic.finance.dtos.UserDTO
import space.luminic.finance.dtos.UserDTO.TelegramAuthDTO
import space.luminic.finance.models.User import space.luminic.finance.models.User
object UserMapper { object UserMapper {
@@ -11,7 +12,18 @@ object UserMapper {
firstName = this.firstName, firstName = this.firstName,
tgId = this.tgId, tgId = this.tgId,
tgUserName = this.tgUserName, tgUserName = this.tgUserName,
photoUrl = this.photoUrl,
roles = this.roles roles = this.roles
) )
fun TelegramAuthDTO.toTelegramMap(): Map<String, String> =
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
)
} }

View File

@@ -11,8 +11,9 @@ data class User(
var id: Int? = null, var id: Int? = null,
val username: String, val username: String,
var firstName: String, var firstName: String,
var tgId: String? = null, var tgId: Long? = null,
var tgUserName: String? = null, var tgUserName: String? = null,
val photoUrl: String? = null,
var password: String? = null, var password: String? = null,
var isActive: Boolean = true, var isActive: Boolean = true,
var regDate: LocalDate = LocalDate.now(), var regDate: LocalDate = LocalDate.now(),

View File

@@ -9,8 +9,8 @@ interface UserRepo {
fun findById(id: Int): User? fun findById(id: Int): User?
fun findByUsername(username: String): User? fun findByUsername(username: String): User?
fun findParticipantsBySpace(spaceId: Int): Set<User> fun findParticipantsBySpace(spaceId: Int): Set<User>
fun findByTgId(tgId: String): User? fun findByTgId(tgId: Long): User?
fun save(user: User): User fun create(user: User): User
fun update(user: User): User fun update(user: User): User
fun deleteById(id: Long) fun deleteById(id: Long)
} }

View File

@@ -4,18 +4,20 @@ import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import space.luminic.finance.models.User import space.luminic.finance.models.User
@Repository @Repository
class UserRepoImpl( class UserRepoImpl(
private val jdbcTemplate: NamedParameterJdbcTemplate private val jdbcTemplate: NamedParameterJdbcTemplate
) : UserRepo{ ) : UserRepo {
private fun userRowMapper() = RowMapper { rs, _ -> private fun userRowMapper() = RowMapper { rs, _ ->
User( User(
id = rs.getInt("id"), id = rs.getInt("id"),
username = rs.getString("username"), username = rs.getString("username"),
firstName = rs.getString("first_name"), firstName = rs.getString("first_name"),
tgId = rs.getString("tg_id"), tgId = rs.getLong("tg_id"),
tgUserName = rs.getString("tg_user_name"), tgUserName = rs.getString("tg_user_name"),
photoUrl = rs.getString("photo_url"),
password = rs.getString("password"), password = rs.getString("password"),
isActive = rs.getBoolean("is_active"), isActive = rs.getBoolean("is_active"),
regDate = rs.getDate("reg_date").toLocalDate(), regDate = rs.getDate("reg_date").toLocalDate(),
@@ -41,11 +43,12 @@ class UserRepoImpl(
} }
override fun findParticipantsBySpace(spaceId: Int): Set<User> { override fun findParticipantsBySpace(spaceId: Int): Set<User> {
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() return jdbcTemplate.query(sql, mapOf("spaceId" to spaceId), userRowMapper()).toSet()
} }
override fun findByTgId(tgId: String): User? { override fun findByTgId(tgId: Long): User? {
val sql = """ val sql = """
select * from finance.users u where tg_id = :tgId select * from finance.users u where tg_id = :tgId
""".trimIndent() """.trimIndent()
@@ -53,13 +56,15 @@ class UserRepoImpl(
return jdbcTemplate.queryForObject(sql, params, userRowMapper()) return jdbcTemplate.queryForObject(sql, params, userRowMapper())
} }
override fun save(user: User): User { override fun create(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" 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( val params = mapOf(
"username" to user.username, "username" to user.username,
"firstname" to user.firstName, "firstname" to user.firstName,
"tg_id" to user.tgId, "tg_id" to user.tgId,
"tg_user_name" to user.tgUserName, "tg_user_name" to user.tgUserName,
"photo_url" to user.photoUrl,
"password" to user.password, "password" to user.password,
"isActive" to user.isActive, "isActive" to user.isActive,
"regDate" to user.regDate, "regDate" to user.regDate,

View File

@@ -6,6 +6,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import space.luminic.finance.configs.AuthException 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.Token
import space.luminic.finance.models.User import space.luminic.finance.models.User
import space.luminic.finance.repos.UserRepo import space.luminic.finance.repos.UserRepo
@@ -61,10 +63,12 @@ class AuthService(
} }
} }
fun tgLogin(tgId: String): String { fun tgAuth(tgUser: UserDTO.TelegramAuthDTO): String {
val user = val user: User = try {
userRepo.findByTgId(tgId) ?: throw UsernameNotFoundException("Пользователь не найден") tgLogin(tgUser.id)
} catch (e: NotFoundException) {
registerTg(tgUser)
}
val token = jwtUtil.generateToken(user.username) val token = jwtUtil.generateToken(user.username)
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10) val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
tokenService.saveToken( tokenService.saveToken(
@@ -73,7 +77,22 @@ class AuthService(
expiresAt = expireAt.toInstant() expiresAt = expireAt.toInstant()
) )
return token 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 { fun register(username: String, password: String, firstName: String): User {
@@ -85,7 +104,7 @@ class AuthService(
firstName = firstName, firstName = firstName,
roles = mutableListOf("USER") roles = mutableListOf("USER")
) )
newUser = userRepo.save(newUser) newUser = userRepo.create(newUser)
return newUser return newUser
} else throw IllegalArgumentException("Пользователь уже зарегистрирован") } else throw IllegalArgumentException("Пользователь уже зарегистрирован")
} }

View File

@@ -24,7 +24,7 @@ class UserService(val userRepo: UserRepo) {
} }
fun getUserByTelegramId(telegramId: Long): User { 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")
} }

View File

@@ -13,7 +13,7 @@ logging.level.org.mongodb.driver.protocol.command = INFO
#management.endpoint.metrics.access=read_only #management.endpoint.metrics.access=read_only
telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY telegram.bot.token = 7999296388:AAGXPE5r0yt3ZFehBoUh8FGm5FBbs9pYIks
nlp.address=https://nlp.luminic.space nlp.address=https://nlp.luminic.space

View File

@@ -0,0 +1,3 @@
alter table finance.users
add column photo_url varchar null,
alter column tg_id set data type numeric;