tg login
This commit is contained in:
1
gradle.properties
Normal file
1
gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
kotlin.code.style=official
|
||||
@@ -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<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")
|
||||
fun test(): String {
|
||||
val authentication = SecurityContextHolder.getContext().authentication
|
||||
@@ -26,23 +70,26 @@ class AuthController(
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
fun login(@RequestBody request: AuthUserDTO): Map<String, String> {
|
||||
fun login(@RequestBody request: AuthUserDTO): Map<String, String> {
|
||||
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<String, String> {
|
||||
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")
|
||||
|
||||
@@ -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<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
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<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
|
||||
)
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -9,8 +9,8 @@ interface UserRepo {
|
||||
fun findById(id: Int): User?
|
||||
fun findByUsername(username: String): User?
|
||||
fun findParticipantsBySpace(spaceId: Int): Set<User>
|
||||
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)
|
||||
}
|
||||
@@ -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<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()
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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()) -> {
|
||||
|
||||
@@ -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")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
3
src/main/resources/db/migration/V22__.sql
Normal file
3
src/main/resources/db/migration/V22__.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
alter table finance.users
|
||||
add column photo_url varchar null,
|
||||
alter column tg_id set data type numeric;
|
||||
Reference in New Issue
Block a user