add auth module
This commit is contained in:
parent
2265170de3
commit
61db7e831f
26
README.md
26
README.md
|
|
@ -122,3 +122,29 @@ Interceptor для логирования запросов/ответов.
|
|||
## settings-spring-jpa
|
||||
|
||||
Модуль для хранения настроек
|
||||
|
||||
## auth-core
|
||||
|
||||
Модуль авторизации
|
||||
|
||||
## auth-jwt-core
|
||||
|
||||
Добавляет поддержку jwt-токенов (создание/хранение). Для работы этого модуля требуется прописать в пропертях:
|
||||
|
||||
``` yaml
|
||||
token.access:
|
||||
issuer: ${app.issuer}
|
||||
timeToLive: PT15M # 15 minutes
|
||||
signatureAlgorithm: RS256
|
||||
keyPair:
|
||||
public: |
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
-----END PUBLIC KEY-----
|
||||
private: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
-----END PRIVATE KEY-----
|
||||
token.refresh:
|
||||
length: 20
|
||||
prefix: RT-
|
||||
timeToLive: PT2H # 2 hours
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
plugins {
|
||||
id("kotlin")
|
||||
id("kotlin-spring")
|
||||
kotlin("plugin.jpa")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
|
||||
api(project(":common"))
|
||||
|
||||
api(project(":common-spring-jpa"))
|
||||
|
||||
api("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
|
||||
implementation("org.liquibase:liquibase-core")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
|
||||
testImplementation(project(":common-spring-test-jpa"))
|
||||
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
|
||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-params")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||
|
||||
testImplementation("org.testcontainers:testcontainers")
|
||||
testImplementation("org.testcontainers:postgresql")
|
||||
testImplementation("org.testcontainers:junit-jupiter")
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.auth.core.configurations
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
|
||||
@ComponentScan("ru.touchin.auth.core")
|
||||
@ConfigurationPropertiesScan
|
||||
class AuthCoreConfiguration {
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return BCryptPasswordEncoder()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.auth.core.configurations
|
||||
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan
|
||||
import org.springframework.cache.annotation.EnableCaching
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
|
||||
import ru.touchin.common.spring.jpa.EnableJpaAuditingExtra
|
||||
|
||||
@EntityScan("ru.touchin.auth.core")
|
||||
@EnableJpaRepositories("ru.touchin.auth.core")
|
||||
@EnableCaching
|
||||
@EnableJpaAuditingExtra
|
||||
class AuthCoreDatabaseConfiguration
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.touchin.auth.core.device.converters
|
||||
|
||||
import ru.touchin.auth.core.device.dto.Device
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
|
||||
object DeviceConverter {
|
||||
|
||||
fun DeviceEntity.toDto(): Device {
|
||||
return Device(
|
||||
id = this.id!!,
|
||||
platform = this.platform,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.auth.core.device.dto
|
||||
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import java.util.*
|
||||
|
||||
data class Device(
|
||||
val id: UUID,
|
||||
val platform: DevicePlatform,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.auth.core.device.dto.enums
|
||||
|
||||
enum class DevicePlatform {
|
||||
Android, Huawei, Apple
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.auth.core.device.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
import java.util.*
|
||||
|
||||
class DeviceAlreadyLinkedException(deviceId: UUID) : CommonException(
|
||||
"Device $deviceId already linked to other user"
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.auth.core.device.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonNotFoundException
|
||||
import java.util.*
|
||||
|
||||
class DeviceNotFoundException(deviceId: UUID) : CommonNotFoundException(
|
||||
"Device not found id=$deviceId"
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package ru.touchin.auth.core.device.models
|
||||
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import ru.touchin.auth.core.user.models.UserEntity
|
||||
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.JoinTable
|
||||
import javax.persistence.ManyToMany
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "devices")
|
||||
class DeviceEntity: AuditableUuidIdEntity() {
|
||||
|
||||
lateinit var platform: DevicePlatform
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "devices_users",
|
||||
joinColumns = [JoinColumn(name = "device_id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "user_id")]
|
||||
)
|
||||
lateinit var users: Set<UserEntity>
|
||||
|
||||
companion object {
|
||||
fun create(platform: DevicePlatform): DeviceEntity {
|
||||
return DeviceEntity().apply {
|
||||
this.platform = platform
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package ru.touchin.auth.core.device.repository
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Lock
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import java.util.*
|
||||
import javax.persistence.LockModeType
|
||||
|
||||
interface DeviceRepository: JpaRepository<DeviceEntity, UUID> {
|
||||
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("SELECT d FROM DeviceEntity d WHERE d.id = :deviceId")
|
||||
fun findByIdWithLock(deviceId: UUID): DeviceEntity?
|
||||
|
||||
}
|
||||
|
||||
fun DeviceRepository.findByIdOrThrow(deviceId: UUID): DeviceEntity {
|
||||
return findByIdOrNull(deviceId)
|
||||
?: throw DeviceNotFoundException(deviceId)
|
||||
}
|
||||
|
||||
fun DeviceRepository.findByIdWithLockOrThrow(deviceId: UUID): DeviceEntity {
|
||||
return findByIdWithLock(deviceId)
|
||||
?: throw DeviceNotFoundException(deviceId)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.auth.core.device.services
|
||||
|
||||
import ru.touchin.auth.core.device.dto.Device
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import java.util.UUID
|
||||
|
||||
interface DeviceCoreService {
|
||||
|
||||
fun get(deviceId: UUID): Device
|
||||
fun create(platform: DevicePlatform): Device
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.auth.core.device.services
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
|
||||
import ru.touchin.auth.core.device.dto.Device
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import ru.touchin.auth.core.device.repository.findByIdOrThrow
|
||||
import java.util.*
|
||||
|
||||
@Service
|
||||
class DeviceCoreServiceImpl(
|
||||
private val deviceRepository: DeviceRepository,
|
||||
) : DeviceCoreService {
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun get(deviceId: UUID): Device {
|
||||
return deviceRepository.findByIdOrThrow(deviceId)
|
||||
.toDto()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun create(platform: DevicePlatform): Device {
|
||||
return deviceRepository.save(DeviceEntity.create(platform))
|
||||
.toDto()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.auth.core.policy.dto
|
||||
|
||||
data class RegistrationPolicy(
|
||||
val multiAccountsPerDevice: Boolean
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package ru.touchin.auth.core.policy.services
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.stereotype.Service
|
||||
import ru.touchin.auth.core.policy.dto.RegistrationPolicy
|
||||
|
||||
@Service
|
||||
class DefaultPolicyServiceImpl : PolicyService {
|
||||
|
||||
override fun getRegistrationPolicy(): RegistrationPolicy {
|
||||
return RegistrationPolicy(
|
||||
multiAccountsPerDevice = false
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.auth.core.policy.services
|
||||
|
||||
import ru.touchin.auth.core.policy.dto.RegistrationPolicy
|
||||
|
||||
interface PolicyService {
|
||||
|
||||
fun getRegistrationPolicy(): RegistrationPolicy
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.auth.core.scope.converters
|
||||
|
||||
import ru.touchin.auth.core.scope.dto.Scope
|
||||
import ru.touchin.auth.core.scope.models.ScopeEntity
|
||||
|
||||
object ScopeConverter {
|
||||
|
||||
fun ScopeEntity.toDto(): Scope {
|
||||
return Scope(name)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.auth.core.scope.dto
|
||||
|
||||
data class Scope(
|
||||
val name: String
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.auth.core.scope.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonNotFoundException
|
||||
|
||||
class ScopeNotFoundException(scope: String) : CommonNotFoundException(
|
||||
"Scope not found name=$scope"
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.touchin.auth.core.scope.models
|
||||
|
||||
import ru.touchin.common.spring.jpa.models.BaseEntity
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "scopes")
|
||||
class ScopeEntity : BaseEntity() {
|
||||
|
||||
@Id
|
||||
lateinit var name: String
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ru.touchin.auth.core.scope.models
|
||||
|
||||
import ru.touchin.common.spring.jpa.models.BaseUuidIdEntity
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "scope_groups")
|
||||
class ScopeGroupEntity : BaseUuidIdEntity() {
|
||||
|
||||
lateinit var groupName: String
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "scope_name")
|
||||
lateinit var scope: ScopeEntity
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_USER_SCOPE_GROUP = "DefaultUser"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.auth.core.scope.repositories
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import ru.touchin.auth.core.scope.models.ScopeGroupEntity
|
||||
|
||||
interface ScopeGroupRepository : JpaRepository<ScopeGroupEntity, String>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package ru.touchin.auth.core.scope.repositories
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import ru.touchin.auth.core.scope.exceptions.ScopeNotFoundException
|
||||
import ru.touchin.auth.core.scope.models.ScopeEntity
|
||||
|
||||
interface ScopeRepository : JpaRepository<ScopeEntity, String> {
|
||||
|
||||
@Query("SELECT sg.scope FROM ScopeGroupEntity sg WHERE sg.groupName = :groupName")
|
||||
fun findByGroup(groupName: String): List<ScopeEntity>
|
||||
|
||||
}
|
||||
|
||||
fun ScopeRepository.findByIdOrThrow(scope: String): ScopeEntity {
|
||||
return findByIdOrNull(scope)
|
||||
?: throw ScopeNotFoundException(scope)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package ru.touchin.auth.core.user.converters
|
||||
|
||||
import ru.touchin.auth.core.device.dto.Device
|
||||
import ru.touchin.auth.core.scope.converters.ScopeConverter.toDto
|
||||
import ru.touchin.auth.core.user.dto.User
|
||||
import ru.touchin.auth.core.user.models.UserEntity
|
||||
|
||||
object UserConverter {
|
||||
|
||||
fun UserEntity.toDto(device: Device?): User {
|
||||
return User(
|
||||
id = id!!,
|
||||
device = device,
|
||||
scopes = scopes.map { it.toDto() }.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.auth.core.user.dto
|
||||
|
||||
import ru.touchin.auth.core.device.dto.Device
|
||||
import ru.touchin.auth.core.scope.dto.Scope
|
||||
import java.util.*
|
||||
|
||||
data class User(
|
||||
val id: UUID,
|
||||
val device: Device?,
|
||||
val scopes: Set<Scope>,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.auth.core.user.dto.enums
|
||||
|
||||
enum class IdentifierType {
|
||||
Username, PhoneNumber, Email
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.auth.core.user.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonNotFoundException
|
||||
|
||||
class UserAccountNotFoundException(username: String) : CommonNotFoundException(
|
||||
"User account not found username=$username"
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.auth.core.user.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class UserAlreadyRegisteredException(phoneNumber: String) : CommonException(
|
||||
"User '$phoneNumber' already registered"
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.auth.core.user.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonNotFoundException
|
||||
import java.util.*
|
||||
|
||||
class UserNotFoundException(userId: UUID) : CommonNotFoundException (
|
||||
"User not found id=$userId"
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.auth.core.user.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class WrongPasswordException(username: String) : CommonException(
|
||||
"Wrong password for user '$username'"
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package ru.touchin.auth.core.user.models
|
||||
|
||||
import ru.touchin.auth.core.user.dto.enums.IdentifierType
|
||||
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
|
||||
import java.time.ZonedDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_accounts")
|
||||
class UserAccountEntity: AuditableUuidIdEntity() {
|
||||
|
||||
lateinit var username: String
|
||||
|
||||
var password: String? = null
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
lateinit var identifierType: IdentifierType
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
lateinit var user: UserEntity
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package ru.touchin.auth.core.user.models
|
||||
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.scope.models.ScopeEntity
|
||||
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
|
||||
import java.time.ZonedDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.JoinTable
|
||||
import javax.persistence.ManyToMany
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
class UserEntity: AuditableUuidIdEntity() {
|
||||
|
||||
var anonymous: Boolean = true
|
||||
|
||||
var confirmedAt: ZonedDateTime? = null
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "devices_users",
|
||||
joinColumns = [JoinColumn(name = "user_id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "device_id")]
|
||||
)
|
||||
lateinit var devices: Set<DeviceEntity>
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "users_scopes",
|
||||
joinColumns = [JoinColumn(name = "user_id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "scope_name")]
|
||||
)
|
||||
lateinit var scopes: Set<ScopeEntity>
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
package ru.touchin.auth.core.user.repositories
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import ru.touchin.auth.core.user.dto.enums.IdentifierType
|
||||
import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException
|
||||
import ru.touchin.auth.core.user.models.UserAccountEntity
|
||||
import java.util.*
|
||||
|
||||
interface UserAccountRepository: JpaRepository<UserAccountEntity, UUID> {
|
||||
|
||||
@Query("""
|
||||
SELECT ua
|
||||
FROM UserAccountEntity ua
|
||||
WHERE ua.username = :username AND identifierType = :identifierType
|
||||
""")
|
||||
fun findByUsername(username: String, identifierType: IdentifierType): UserAccountEntity?
|
||||
|
||||
}
|
||||
|
||||
fun UserAccountRepository.findByUsernameOrThrow(username: String, identifierType: IdentifierType): UserAccountEntity {
|
||||
return findByUsername(username, identifierType)
|
||||
?: throw UserAccountNotFoundException(username)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.touchin.auth.core.user.repositories
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import ru.touchin.auth.core.user.exceptions.UserNotFoundException
|
||||
import ru.touchin.auth.core.user.models.UserEntity
|
||||
import java.util.*
|
||||
|
||||
interface UserRepository: JpaRepository<UserEntity, UUID>
|
||||
|
||||
fun UserRepository.findByIdOrThrow(userId: UUID): UserEntity {
|
||||
return findByIdOrNull(userId)
|
||||
?: throw UserNotFoundException(userId)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package ru.touchin.auth.core.user.services
|
||||
|
||||
import ru.touchin.auth.core.user.dto.User
|
||||
import ru.touchin.auth.core.user.dto.enums.IdentifierType
|
||||
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser
|
||||
import ru.touchin.auth.core.user.services.dto.NewUser
|
||||
import ru.touchin.auth.core.user.services.dto.UserLogin
|
||||
|
||||
interface UserCoreService {
|
||||
|
||||
fun create(newAnonymousUser: NewAnonymousUser): User
|
||||
fun create(newUser: NewUser): User
|
||||
fun get(username: String, identifierType: IdentifierType): User
|
||||
fun getOrNull(username: String, identifierType: IdentifierType): User?
|
||||
fun login(userLogin: UserLogin): User
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.auth.core.user.services
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
|
||||
import ru.touchin.auth.core.device.exceptions.DeviceAlreadyLinkedException
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow
|
||||
import ru.touchin.auth.core.scope.models.ScopeGroupEntity
|
||||
import ru.touchin.auth.core.scope.repositories.ScopeRepository
|
||||
import ru.touchin.auth.core.user.converters.UserConverter.toDto
|
||||
import ru.touchin.auth.core.user.dto.User
|
||||
import ru.touchin.auth.core.user.dto.enums.IdentifierType
|
||||
import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException
|
||||
import ru.touchin.auth.core.user.exceptions.UserAlreadyRegisteredException
|
||||
import ru.touchin.auth.core.user.exceptions.WrongPasswordException
|
||||
import ru.touchin.auth.core.user.models.UserAccountEntity
|
||||
import ru.touchin.auth.core.user.models.UserEntity
|
||||
import ru.touchin.auth.core.user.repositories.UserAccountRepository
|
||||
import ru.touchin.auth.core.user.repositories.UserRepository
|
||||
import ru.touchin.auth.core.user.repositories.findByUsernameOrThrow
|
||||
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser
|
||||
import ru.touchin.auth.core.user.services.dto.NewUser
|
||||
import ru.touchin.auth.core.user.services.dto.UserLogin
|
||||
|
||||
@Service
|
||||
class UserCoreServiceImpl(
|
||||
private val userRepository: UserRepository,
|
||||
private val userAccountRepository: UserAccountRepository,
|
||||
private val deviceRepository: DeviceRepository,
|
||||
private val scopeRepository: ScopeRepository,
|
||||
private val passwordEncoder: PasswordEncoder,
|
||||
) : UserCoreService {
|
||||
|
||||
@Transactional
|
||||
override fun create(newAnonymousUser: NewAnonymousUser): User {
|
||||
val device = deviceRepository.findByIdWithLockOrThrow(newAnonymousUser.deviceId)
|
||||
|
||||
if (device.users.isNotEmpty()) {
|
||||
throw DeviceAlreadyLinkedException(newAnonymousUser.deviceId)
|
||||
}
|
||||
|
||||
val user = UserEntity().apply {
|
||||
anonymous = true
|
||||
devices = hashSetOf(device)
|
||||
scopes = emptySet()
|
||||
}
|
||||
|
||||
return userRepository.save(user)
|
||||
.toDto(device.toDto())
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun create(newUser: NewUser): User {
|
||||
userAccountRepository.findByUsername(newUser.username, newUser.identifierType)
|
||||
?.run { throw UserAlreadyRegisteredException(newUser.username) }
|
||||
|
||||
val device = deviceRepository.findByIdWithLockOrThrow(newUser.deviceId)
|
||||
|
||||
resetDeviceUsers(device)
|
||||
|
||||
val defaultScopes = scopeRepository.findByGroup(ScopeGroupEntity.DEFAULT_USER_SCOPE_GROUP)
|
||||
|
||||
val user = UserEntity()
|
||||
.apply {
|
||||
anonymous = false
|
||||
devices = hashSetOf(device)
|
||||
scopes = defaultScopes.toSet()
|
||||
}
|
||||
.also(userRepository::save)
|
||||
|
||||
UserAccountEntity()
|
||||
.apply {
|
||||
username = newUser.username
|
||||
password = newUser.password?.let(passwordEncoder::encode)
|
||||
identifierType = newUser.identifierType
|
||||
this.user = user
|
||||
}
|
||||
.also(userAccountRepository::save)
|
||||
|
||||
return user.toDto(device.toDto())
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun login(userLogin: UserLogin): User {
|
||||
val device = deviceRepository.findByIdWithLockOrThrow(userLogin.deviceId)
|
||||
|
||||
val userAccount = userAccountRepository.findByUsernameOrThrow(userLogin.username, userLogin.identifierType)
|
||||
|
||||
if (userAccount.password != null) {
|
||||
if (!passwordEncoder.matches(userLogin.password, userAccount.password!!)) {
|
||||
throw WrongPasswordException(userLogin.username)
|
||||
}
|
||||
}
|
||||
|
||||
resetDeviceUsers(device)
|
||||
|
||||
val user = userAccount.user
|
||||
.apply {
|
||||
devices = hashSetOf(device)
|
||||
}
|
||||
.also(userRepository::save)
|
||||
|
||||
return user.toDto(device.toDto())
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun get(username: String, identifierType: IdentifierType): User {
|
||||
return getOrNull(username, identifierType)
|
||||
?: throw UserAccountNotFoundException(username)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun getOrNull(username: String, identifierType: IdentifierType): User? {
|
||||
return userAccountRepository.findByUsername(username, identifierType)
|
||||
?.let { userAccount ->
|
||||
val user = userAccount.user
|
||||
|
||||
user.toDto(device = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetDeviceUsers(device: DeviceEntity) {
|
||||
deviceRepository.save(device.apply {
|
||||
users = hashSetOf()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.auth.core.user.services.dto
|
||||
|
||||
import java.util.*
|
||||
|
||||
data class NewAnonymousUser(
|
||||
val deviceId: UUID
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.auth.core.user.services.dto
|
||||
|
||||
import ru.touchin.auth.core.user.dto.enums.IdentifierType
|
||||
import java.util.*
|
||||
|
||||
data class NewUser(
|
||||
val deviceId: UUID,
|
||||
val identifierType: IdentifierType,
|
||||
val username: String,
|
||||
val password: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.auth.core.user.services.dto
|
||||
|
||||
import ru.touchin.auth.core.user.dto.enums.IdentifierType
|
||||
import java.util.*
|
||||
|
||||
data class UserLogin (
|
||||
val deviceId: UUID,
|
||||
val identifierType: IdentifierType,
|
||||
val username: String,
|
||||
val password: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105011900__create_table__devices
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: devices
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: devices
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: pk_devices
|
||||
- column:
|
||||
name: platform
|
||||
type: VARCHAR(16)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: created_by
|
||||
type: VARCHAR(255)
|
||||
- column:
|
||||
name: updated_by
|
||||
type: VARCHAR(255)
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
defaultValueDate: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105052300__create_table__users
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: users
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: users
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: pk_users
|
||||
- column:
|
||||
name: anonymous
|
||||
type: BOOLEAN
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: confirmed_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: created_by
|
||||
type: VARCHAR(255)
|
||||
- column:
|
||||
name: updated_by
|
||||
type: VARCHAR(255)
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
defaultValueDate: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105052310__create_table__user_accounts
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: user_accounts
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: user_accounts
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: pk_user_accounts
|
||||
- column:
|
||||
name: username
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: password
|
||||
type: VARCHAR(255)
|
||||
- column:
|
||||
name: identifier_type
|
||||
type: VARCHAR(32)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: user_id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
foreignKeyName: fk_user_accounts_users
|
||||
references: users(id)
|
||||
- column:
|
||||
name: created_by
|
||||
type: VARCHAR(255)
|
||||
- column:
|
||||
name: updated_by
|
||||
type: VARCHAR(255)
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
defaultValueDate: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
- createIndex:
|
||||
tableName: user_accounts
|
||||
columns:
|
||||
- column:
|
||||
name: username
|
||||
indexName: idx_user_accounts_username
|
||||
unique: true
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105052331__create_table__devices_users
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: devices_users
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: devices_users
|
||||
columns:
|
||||
- column:
|
||||
name: device_id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
references: devices(id)
|
||||
foreignKeyName: fk_devices_users_device_id
|
||||
- column:
|
||||
name: user_id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
references: users(id)
|
||||
foreignKeyName: fk_devices_users_user_id
|
||||
- addPrimaryKey:
|
||||
tableName: devices_users
|
||||
columnNames: device_id, user_id
|
||||
constraintName: pk_devices_users
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105081411__create_table__scopes
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: scopes
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: scopes
|
||||
columns:
|
||||
- column:
|
||||
name: name
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
primaryKey: true
|
||||
primaryKeyName: pk_scopes
|
||||
nullable: false
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
defaultValueDate: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
- insert:
|
||||
tableName: scopes
|
||||
columns:
|
||||
- column:
|
||||
name: name
|
||||
value: "app:api"
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105081431__create_table__scope_groups
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: scope_groups
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: scope_groups
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: pk_scope_groups
|
||||
- column:
|
||||
name: group_name
|
||||
type: VARCHAR(64)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: scope_name
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
references: scopes(name)
|
||||
foreignKeyName: fk_scope_groups_scopes_scope_name
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
defaultValueDate: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
- insert:
|
||||
tableName: scope_groups
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: "0f08f4af-0e59-4914-b25f-291889572152"
|
||||
- column:
|
||||
name: group_name
|
||||
value: "DefaultUser"
|
||||
- column:
|
||||
name: scope_name
|
||||
value: "app:api"
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105081531__create_table__users_scopes
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: users_scopes
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: users_scopes
|
||||
columns:
|
||||
- column:
|
||||
name: user_id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
references: users(id)
|
||||
foreignKeyName: fk_users_scopes_user_id
|
||||
- column:
|
||||
name: scope_name
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
references: scopes(name)
|
||||
foreignKeyName: fk_users_scopes_scope_name
|
||||
- addPrimaryKey:
|
||||
tableName: users_scopes
|
||||
columnNames: user_id, scope_name
|
||||
constraintName: pk_users_scopes
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.touchin.auth.core
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.context.annotation.Profile
|
||||
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration
|
||||
|
||||
@Profile("test-slow")
|
||||
@EnableAutoConfiguration
|
||||
@TestConfiguration
|
||||
@ComponentScan
|
||||
@Import(AuthCoreDatabaseConfiguration::class)
|
||||
class AuthCoreSlowTestConfiguration
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ru.touchin.auth.core
|
||||
|
||||
import org.springframework.boot.SpringBootConfiguration
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import ru.touchin.auth.core.configurations.AuthCoreConfiguration
|
||||
|
||||
@SpringBootConfiguration
|
||||
@ContextConfiguration(classes = [AuthCoreConfiguration::class])
|
||||
@TestConfiguration
|
||||
@Import(AuthCoreConfiguration::class, AuthCoreSlowTestConfiguration::class)
|
||||
class AuthCoreTestApplication
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package ru.touchin.auth.core.device.repositories
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Propagation
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
@Service
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
class DeviceLockService {
|
||||
|
||||
@Autowired
|
||||
private lateinit var deviceRepository: DeviceRepository
|
||||
|
||||
fun getWithLock(id: UUID, pauseAfter: Duration): DeviceEntity? {
|
||||
val device = deviceRepository.findByIdWithLockOrThrow(id)
|
||||
|
||||
Thread.sleep(pauseAfter.toMillis())
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
fun create(f: () -> DeviceEntity): DeviceEntity {
|
||||
return f.invoke()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
deviceRepository.deleteAll()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package ru.touchin.auth.core.device.repositories
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.dao.DataAccessException
|
||||
import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import ru.touchin.auth.core.device.repository.findByIdOrThrow
|
||||
import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow
|
||||
import ru.touchin.auth.core.user.models.UserEntity
|
||||
import ru.touchin.auth.core.user.repositories.UserRepository
|
||||
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@RepositoryTest
|
||||
internal class DeviceRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var deviceLockService: DeviceLockService
|
||||
|
||||
@Autowired
|
||||
private lateinit var deviceRepository: DeviceRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var userRepository: UserRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var entityManager: EntityManager
|
||||
|
||||
fun createDevice(platform: DevicePlatform): DeviceEntity {
|
||||
return createDevice(DeviceEntity.create(platform))
|
||||
}
|
||||
|
||||
fun createDevice(deviceModel: DeviceEntity): DeviceEntity {
|
||||
return deviceRepository.saveAndFlush(deviceModel)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Можно создать `DeviceModel` для всех платформ без привязки к пользователю")
|
||||
fun deviceShouldBeCreated() {
|
||||
DevicePlatform.values().forEach(::createDevice)
|
||||
|
||||
entityManager.clear()
|
||||
|
||||
val createdPlatforms = deviceRepository.findAll()
|
||||
.map(DeviceEntity::platform)
|
||||
|
||||
assertEquals(
|
||||
DevicePlatform.values().size,
|
||||
createdPlatforms.size,
|
||||
"Кол-во записей в базе должно совпадать с кол-вом платформ"
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DevicePlatform.values().toSet(),
|
||||
createdPlatforms.toSet(),
|
||||
"Платформы в базе должны совпадать со всеми `DevicePlatform`"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Можно привязать существующего пользователя")
|
||||
fun canBindUser() {
|
||||
val user = userRepository.save(UserEntity())
|
||||
|
||||
val device = DeviceEntity.create(DevicePlatform.Apple)
|
||||
.apply {
|
||||
users = setOf(user)
|
||||
}
|
||||
|
||||
deviceRepository.saveAndFlush(device)
|
||||
|
||||
entityManager.clear()
|
||||
|
||||
val savedDevice = deviceRepository.findByIdOrThrow(device.id!!)
|
||||
|
||||
assertEquals(1, savedDevice.users.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должна быть ошибка при сохранении устройства с новым пользователем")
|
||||
fun deviceShouldNotBeCreatedWithNewUser() {
|
||||
val device = DeviceEntity.create(DevicePlatform.Android)
|
||||
.apply {
|
||||
users = setOf(UserEntity())
|
||||
}
|
||||
|
||||
assertThrows(DataAccessException::class.java) {
|
||||
deviceRepository.saveAndFlush(device)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если девайс не найден, то должна быть ошибка DeviceNotFoundException")
|
||||
fun shouldBeDeviceNotFoundException() {
|
||||
val missingDeviceId = UUID.fromString("d3e957df-6686-421e-9807-7128c8e680ea")
|
||||
|
||||
assertThrows(DeviceNotFoundException::class.java) {
|
||||
deviceRepository.findByIdOrThrow(missingDeviceId)
|
||||
}
|
||||
|
||||
assertThrows(DeviceNotFoundException::class.java) {
|
||||
deviceRepository.findByIdWithLockOrThrow(missingDeviceId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должен работать Lock")
|
||||
fun shouldBeLockable() {
|
||||
val device = deviceLockService.create {
|
||||
createDevice(DevicePlatform.Huawei)
|
||||
}
|
||||
|
||||
val threadsCount = 2
|
||||
|
||||
val service = Executors.newFixedThreadPool(threadsCount)
|
||||
val latch = CountDownLatch(threadsCount)
|
||||
|
||||
val seq = ConcurrentLinkedQueue<Int>()
|
||||
|
||||
service.submit {
|
||||
deviceLockService.getWithLock(id = device.id!!, Duration.ofSeconds(1))
|
||||
latch.countDown()
|
||||
seq.add(1)
|
||||
}
|
||||
|
||||
Thread.sleep(50)
|
||||
|
||||
service.submit {
|
||||
deviceLockService.getWithLock(id = device.id!!, Duration.ofSeconds(0))
|
||||
latch.countDown()
|
||||
seq.add(2)
|
||||
}
|
||||
|
||||
latch.await(3, TimeUnit.SECONDS)
|
||||
|
||||
deviceLockService.cleanup()
|
||||
|
||||
assertEquals(1, seq.poll())
|
||||
assertEquals(2, seq.poll())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package ru.touchin.auth.core.device.services
|
||||
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.spy
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import ru.touchin.auth.core.device.dto.Device
|
||||
import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import java.util.*
|
||||
|
||||
@ActiveProfiles("test")
|
||||
@SpringBootTest
|
||||
internal class DeviceCoreServiceImplTest {
|
||||
|
||||
private var deviceRepository: DeviceRepository = mock {}
|
||||
|
||||
private var deviceCoreService: DeviceCoreService = spy(
|
||||
DeviceCoreServiceImpl(deviceRepository)
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("Device должен создаваться в базе")
|
||||
fun deviceShouldBeCreated() {
|
||||
val deviceId = UUID.fromString("0d3ded83-02c7-4bae-9cd2-3076e0bce046")
|
||||
doAnswer { invocation ->
|
||||
(invocation.getArgument(0) as DeviceEntity)
|
||||
.apply {
|
||||
id = deviceId
|
||||
}
|
||||
}.`when`(deviceRepository).save(any())
|
||||
|
||||
val actualDevice = deviceCoreService.create(DevicePlatform.Huawei)
|
||||
|
||||
val expectedDevice = Device(
|
||||
id = deviceId,
|
||||
platform = DevicePlatform.Huawei,
|
||||
)
|
||||
|
||||
assertEquals(expectedDevice, actualDevice)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Device можно получить через сервис")
|
||||
fun deviceShouldBeGet() {
|
||||
val deviceId = UUID.fromString("0d3ded83-02c7-4bae-9cd2-3076e0bce046")
|
||||
|
||||
doReturn(
|
||||
DeviceEntity()
|
||||
.apply {
|
||||
id = deviceId
|
||||
platform = DevicePlatform.Android
|
||||
}
|
||||
.let { device ->
|
||||
Optional.of(device)
|
||||
}
|
||||
).`when`(deviceRepository).findById(deviceId)
|
||||
|
||||
val actualDevice = deviceCoreService.get(deviceId)
|
||||
|
||||
val expectedDevice = Device(
|
||||
id = deviceId,
|
||||
platform = DevicePlatform.Android,
|
||||
)
|
||||
|
||||
assertEquals(expectedDevice, actualDevice)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Для несуществующих устройств должно быть брошено исключение DeviceNotFoundException")
|
||||
fun deviceShouldBeNotFound() {
|
||||
val missingDeviceId = UUID.fromString("d3e957df-6686-421e-9807-7128c8e680ea")
|
||||
|
||||
doReturn(
|
||||
Optional.empty<DeviceEntity>()
|
||||
).`when`(deviceRepository).findById(any())
|
||||
|
||||
assertThrows<DeviceNotFoundException> {
|
||||
deviceCoreService.get(missingDeviceId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package ru.touchin.auth.core.scope.repositories
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.internal.matchers.apachecommons.ReflectionEquals
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import ru.touchin.auth.core.scope.exceptions.ScopeNotFoundException
|
||||
import ru.touchin.auth.core.scope.models.ScopeEntity
|
||||
import ru.touchin.auth.core.scope.models.ScopeGroupEntity
|
||||
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
|
||||
import java.time.ZonedDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@RepositoryTest
|
||||
internal class ScopeRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var scopeRepository: ScopeRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var scopeGroupRepository: ScopeGroupRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var entityManager: EntityManager
|
||||
|
||||
fun createScope(): ScopeEntity {
|
||||
return ScopeEntity()
|
||||
.apply {
|
||||
name = "admin"
|
||||
createdAt = ZonedDateTime.now()
|
||||
}
|
||||
.also { scope ->
|
||||
scopeRepository.saveAndFlush(scope)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Можно создать `ScopeModel`")
|
||||
fun scopeShouldBeCreated() {
|
||||
val scope = createScope()
|
||||
|
||||
entityManager.clear()
|
||||
|
||||
val savedScope = scopeRepository.findByIdOrThrow(scope.name)
|
||||
|
||||
assertTrue(
|
||||
ReflectionEquals(scope, "createdAt").matches(savedScope)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Имя scope должно быть уникалным")
|
||||
fun scopeShouldBeUniqName() {
|
||||
scopeGroupRepository.deleteAll()
|
||||
scopeRepository.deleteAll()
|
||||
|
||||
createScope()
|
||||
|
||||
entityManager.clear()
|
||||
|
||||
createScope()
|
||||
|
||||
entityManager.clear()
|
||||
|
||||
val scopes = scopeRepository.findAll()
|
||||
|
||||
assertEquals(1, scopes.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если scope не найден, то должна быть ошибка ScopeNotFoundException")
|
||||
fun shouldBeScopeNotFoundException() {
|
||||
val missingScope = "missing"
|
||||
|
||||
assertThrows(ScopeNotFoundException::class.java) {
|
||||
scopeRepository.findByIdOrThrow(missingScope)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должен быть дефолтный scope")
|
||||
fun shouldBeDefaultScope() {
|
||||
val scopes = scopeRepository.findByGroup(ScopeGroupEntity.DEFAULT_USER_SCOPE_GROUP)
|
||||
|
||||
assertEquals(1, scopes.size)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package ru.touchin.auth.core.user.repositories
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.internal.matchers.apachecommons.ReflectionEquals
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.dao.DataAccessException
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import ru.touchin.auth.core.scope.models.ScopeEntity
|
||||
import ru.touchin.auth.core.scope.repositories.ScopeRepository
|
||||
import ru.touchin.auth.core.user.exceptions.UserNotFoundException
|
||||
import ru.touchin.auth.core.user.models.UserEntity
|
||||
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.*
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@RepositoryTest
|
||||
internal class UserRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var userRepository: UserRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var deviceRepository: DeviceRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var scopeRepository: ScopeRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var entityManager: EntityManager
|
||||
|
||||
@Test
|
||||
@DisplayName("Можно создать `UserModel` без привязки к устройствам или скоупам")
|
||||
fun userShouldBeCreated() {
|
||||
val user = userRepository.saveAndFlush(
|
||||
UserEntity().apply {
|
||||
anonymous = false
|
||||
confirmedAt = ZonedDateTime.now()
|
||||
devices = emptySet()
|
||||
scopes = emptySet()
|
||||
}
|
||||
)
|
||||
|
||||
entityManager.clear()
|
||||
|
||||
val savedUser = userRepository.findByIdOrThrow(user.id!!)
|
||||
|
||||
assertTrue(ReflectionEquals(user, "createdAt").matches(savedUser))
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Можно привязать к пользователю существующий device")
|
||||
fun canBindDevice() {
|
||||
val device = deviceRepository.saveAndFlush(
|
||||
DeviceEntity.create(DevicePlatform.Apple)
|
||||
)
|
||||
|
||||
val user = UserEntity()
|
||||
.apply {
|
||||
devices = setOf(device)
|
||||
}
|
||||
|
||||
userRepository.saveAndFlush(user)
|
||||
|
||||
entityManager.clear()
|
||||
|
||||
val savedUser = userRepository.findByIdOrThrow(user.id!!)
|
||||
|
||||
assertEquals(1, savedUser.devices.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Можно привязать к пользователю существующий scope")
|
||||
fun canBindScope() {
|
||||
val scope = scopeRepository.saveAndFlush(
|
||||
ScopeEntity().apply {
|
||||
name = "admin"
|
||||
}
|
||||
)
|
||||
|
||||
val user = UserEntity()
|
||||
.apply {
|
||||
scopes = setOf(scope)
|
||||
}
|
||||
|
||||
userRepository.saveAndFlush(user)
|
||||
|
||||
entityManager.clear()
|
||||
|
||||
val savedUser = userRepository.findByIdOrThrow(user.id!!)
|
||||
|
||||
assertEquals(1, savedUser.scopes.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должна быть ошибка при сохранении пользователя с новым устройством")
|
||||
fun shouldBeErrorIfDeviceNew() {
|
||||
val user = UserEntity()
|
||||
.apply {
|
||||
devices = setOf(DeviceEntity.create(DevicePlatform.Huawei))
|
||||
}
|
||||
|
||||
assertThrows(DataAccessException::class.java) {
|
||||
userRepository.saveAndFlush(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должна быть ошибка при сохранении пользователя с новым scope")
|
||||
fun shouldBeErrorIfScopeNew() {
|
||||
val user = UserEntity()
|
||||
.apply {
|
||||
scopes = setOf(ScopeEntity().apply { name = "admin" })
|
||||
}
|
||||
|
||||
assertThrows(DataAccessException::class.java) {
|
||||
userRepository.saveAndFlush(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если пользователь не найден, то должна быть ошибка UserNotFoundException")
|
||||
fun shouldBeUserNotFoundException() {
|
||||
val missingUserId = UUID.fromString("d3e957df-6686-421e-9807-7128c8e680ea")
|
||||
|
||||
assertThrows(UserNotFoundException::class.java) {
|
||||
userRepository.findByIdOrThrow(missingUserId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
package ru.touchin.auth.core.user.services
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import ru.touchin.auth.core.device.dto.Device
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import ru.touchin.auth.core.device.repository.findByIdOrThrow
|
||||
import ru.touchin.auth.core.device.services.DeviceCoreService
|
||||
import ru.touchin.auth.core.user.dto.User
|
||||
import ru.touchin.auth.core.user.dto.enums.IdentifierType
|
||||
import ru.touchin.auth.core.user.exceptions.UserAlreadyRegisteredException
|
||||
import ru.touchin.auth.core.user.exceptions.WrongPasswordException
|
||||
import ru.touchin.auth.core.user.repositories.UserAccountRepository
|
||||
import ru.touchin.auth.core.user.repositories.UserRepository
|
||||
import ru.touchin.auth.core.user.repositories.findByIdOrThrow
|
||||
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser
|
||||
import ru.touchin.auth.core.user.services.dto.NewUser
|
||||
import ru.touchin.auth.core.user.services.dto.UserLogin
|
||||
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.*
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@RepositoryTest
|
||||
internal class UserCoreServiceImplSlowTest {
|
||||
|
||||
@Autowired
|
||||
lateinit var userCoreService: UserCoreService
|
||||
|
||||
@Autowired
|
||||
lateinit var userRepository: UserRepository
|
||||
|
||||
@Autowired
|
||||
lateinit var deviceCoreService: DeviceCoreService
|
||||
|
||||
@Autowired
|
||||
lateinit var deviceRepository: DeviceRepository
|
||||
|
||||
@Autowired
|
||||
lateinit var userAccountRepository: UserAccountRepository
|
||||
|
||||
@Autowired
|
||||
lateinit var entityManager: EntityManager
|
||||
|
||||
@Autowired
|
||||
lateinit var passwordEncoder: PasswordEncoder
|
||||
|
||||
private fun createDevice(): Device {
|
||||
return deviceCoreService.create(DevicePlatform.Android).also {
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNewUser(deviceId: UUID, password: String? = null, username: String = "manager"): User {
|
||||
val newUser = NewUser(
|
||||
deviceId = deviceId,
|
||||
username = username,
|
||||
password = password,
|
||||
identifierType = IdentifierType.Email,
|
||||
)
|
||||
|
||||
return userCoreService.create(newUser).also {
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Username должен быть уникальным для одного и того же IdentifierType")
|
||||
fun usernameShouldBeUniq() {
|
||||
val device1 = createDevice()
|
||||
|
||||
createNewUser(device1.id, password = null)
|
||||
|
||||
val device2 = createDevice()
|
||||
|
||||
assertThrows<UserAlreadyRegisteredException> {
|
||||
createNewUser(device2.id, password = "qwerty")
|
||||
}
|
||||
|
||||
assertThrows<UserAlreadyRegisteredException> {
|
||||
createNewUser(
|
||||
deviceId = UUID.fromString("2ecc93c9-fccb-4cc9-8d69-18a39b83e707"),
|
||||
password = "qwerty"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("""
|
||||
Анонимные пользователи должны отлинковываться от устройства на которое регистрируется обычный пользователь
|
||||
""")
|
||||
fun anonymousUsersShouldBeUnbinded() {
|
||||
val device = createDevice()
|
||||
|
||||
val anonymousUser = userCoreService.create(
|
||||
NewAnonymousUser(device.id)
|
||||
).also {
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
val anonymousUserEntity = userRepository.findByIdOrThrow(anonymousUser.id)
|
||||
|
||||
assertEquals(1, anonymousUserEntity.devices.size)
|
||||
assertEquals(device.id, anonymousUserEntity.devices.first().id!!)
|
||||
|
||||
val user = createNewUser(device.id)
|
||||
|
||||
val deviceEntity = deviceRepository.findByIdOrThrow(device.id)
|
||||
|
||||
assertEquals(1, deviceEntity.users.size)
|
||||
assertEquals(user.id, deviceEntity.users.first().id!!)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("""
|
||||
Обычные пользователи должны отлинковываться от устройства на которое регистрируется обычный пользователь
|
||||
""")
|
||||
fun usersShouldBeUnbinded() {
|
||||
val device = createDevice()
|
||||
|
||||
val apiUser = createNewUser(device.id, username = "user@gmail.com")
|
||||
|
||||
val anonymousUserEntity = userRepository.findByIdOrThrow(apiUser.id)
|
||||
|
||||
assertEquals(1, anonymousUserEntity.devices.size)
|
||||
assertEquals(device.id, anonymousUserEntity.devices.first().id!!)
|
||||
|
||||
val user = createNewUser(device.id, username = "manager@gmail.com")
|
||||
|
||||
val deviceEntity = deviceRepository.findByIdOrThrow(device.id)
|
||||
|
||||
assertEquals(1, deviceEntity.users.size)
|
||||
assertEquals(user.id, deviceEntity.users.first().id!!)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("""
|
||||
При регистрации пользователя, должен создаваться аккаунт в базе
|
||||
""")
|
||||
fun accountShouldBeCreated() {
|
||||
val device = createDevice()
|
||||
|
||||
val newUser = NewUser(
|
||||
deviceId = device.id,
|
||||
username = "+71232322023",
|
||||
password = "qwerty",
|
||||
identifierType = IdentifierType.PhoneNumber,
|
||||
)
|
||||
|
||||
userCoreService.create(newUser).also {
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
val account = userAccountRepository.findByUsername(newUser.username, IdentifierType.PhoneNumber)
|
||||
|
||||
assertNotNull(account)
|
||||
assertEquals(newUser.username, account!!.username)
|
||||
assertTrue(passwordEncoder.matches(newUser.password!!, account.password!!))
|
||||
assertEquals(newUser.identifierType, account.identifierType)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должен быть успешный логин с паролем")
|
||||
fun loginShouldBeOk() {
|
||||
val device = createDevice()
|
||||
|
||||
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee" )
|
||||
|
||||
val logingUser = userCoreService.login(
|
||||
UserLogin(
|
||||
deviceId = device.id,
|
||||
username = "employee",
|
||||
password = "qwerty",
|
||||
identifierType = IdentifierType.Email,
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(regUser, logingUser)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Пустой пароль должен подходить")
|
||||
fun emptyPassLoginShouldBeOk() {
|
||||
val device = createDevice()
|
||||
|
||||
createNewUser(deviceId = device.id, password = null, username = "employee")
|
||||
|
||||
assertDoesNotThrow {
|
||||
userCoreService.login(
|
||||
UserLogin(
|
||||
deviceId = device.id,
|
||||
username = "employee",
|
||||
password = null,
|
||||
identifierType = IdentifierType.Email,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Пустой пароль не должен подходиить")
|
||||
fun emptyPassLoginShouldBeFail() {
|
||||
val device = createDevice()
|
||||
|
||||
createNewUser(deviceId = device.id, password = "qwerty", username = "employee")
|
||||
|
||||
assertThrows<IllegalArgumentException> {
|
||||
userCoreService.login(
|
||||
UserLogin(
|
||||
deviceId = device.id,
|
||||
username = "employee",
|
||||
password = null,
|
||||
identifierType = IdentifierType.Email,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если пароль не подходит, то WrongPasswordException")
|
||||
fun shouldBeWrongPasswordException() {
|
||||
val device = createDevice()
|
||||
|
||||
createNewUser(deviceId = device.id, password = "qwerty", username = "employee")
|
||||
|
||||
assertThrows<WrongPasswordException> {
|
||||
userCoreService.login(
|
||||
UserLogin(
|
||||
deviceId = device.id,
|
||||
username = "employee",
|
||||
password = "qwerTy",
|
||||
identifierType = IdentifierType.Email,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("При логине старые пользователи должны отлинковываться от устройства")
|
||||
fun prevUsersShouldBeUnbindedAfterLogin() {
|
||||
val device = createDevice()
|
||||
|
||||
createNewUser(deviceId = device.id, password = "qwerty", username = "employee1")
|
||||
createNewUser(deviceId = device.id, password = "qwerty", username = "employee2")
|
||||
val regUserE3 = createNewUser(deviceId = device.id, password = "qwerty", username = "employee3")
|
||||
|
||||
val actualDeviceE3 = deviceRepository.findByIdOrThrow(deviceId = device.id)
|
||||
|
||||
assertEquals(1, actualDeviceE3.users.size)
|
||||
assertEquals(regUserE3.id, actualDeviceE3.users.first().id!!)
|
||||
|
||||
val loginUserE2 = userCoreService.login(
|
||||
UserLogin(
|
||||
deviceId = device.id,
|
||||
username = "employee2",
|
||||
password = "qwerty",
|
||||
identifierType = IdentifierType.Email,
|
||||
)
|
||||
).also {
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
val actualDeviceE2 = deviceRepository.findByIdOrThrow(deviceId = device.id)
|
||||
|
||||
assertEquals(1, actualDeviceE2.users.size)
|
||||
assertEquals(loginUserE2.id, actualDeviceE2.users.first().id!!)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
package ru.touchin.auth.core.user.services
|
||||
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argThat
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.spy
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
|
||||
import ru.touchin.auth.core.device.exceptions.DeviceAlreadyLinkedException
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import ru.touchin.auth.core.policy.dto.RegistrationPolicy
|
||||
import ru.touchin.auth.core.policy.services.PolicyService
|
||||
import ru.touchin.auth.core.scope.dto.Scope
|
||||
import ru.touchin.auth.core.scope.models.ScopeEntity
|
||||
import ru.touchin.auth.core.scope.repositories.ScopeRepository
|
||||
import ru.touchin.auth.core.user.dto.User
|
||||
import ru.touchin.auth.core.user.dto.enums.IdentifierType
|
||||
import ru.touchin.auth.core.user.models.UserEntity
|
||||
import ru.touchin.auth.core.user.repositories.UserAccountRepository
|
||||
import ru.touchin.auth.core.user.repositories.UserRepository
|
||||
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser
|
||||
import ru.touchin.auth.core.user.services.dto.NewUser
|
||||
import java.util.*
|
||||
|
||||
@ActiveProfiles("test")
|
||||
@SpringBootTest
|
||||
internal class UserCoreServiceImplTest {
|
||||
|
||||
private var userRepository: UserRepository = mock {}
|
||||
private var userAccountRepository: UserAccountRepository = mock {}
|
||||
private var deviceRepository: DeviceRepository = mock {}
|
||||
private var scopeRepository: ScopeRepository = mock {}
|
||||
|
||||
private var userCoreService: UserCoreService = spy(UserCoreServiceImpl(
|
||||
userRepository = userRepository,
|
||||
userAccountRepository = userAccountRepository,
|
||||
deviceRepository = deviceRepository,
|
||||
scopeRepository = scopeRepository,
|
||||
passwordEncoder = BCryptPasswordEncoder(),
|
||||
))
|
||||
|
||||
private val deviceWithoutUser = DeviceEntity().apply {
|
||||
id = UUID.fromString("0d3ded83-02c7-4bae-9cd2-3076e0bce046")
|
||||
platform = DevicePlatform.Android
|
||||
users = emptySet()
|
||||
}
|
||||
|
||||
private val deviceWithAnonymousUser = DeviceEntity().apply {
|
||||
id = UUID.fromString("1ba666de-a0c9-4656-900c-864ebf1c0640")
|
||||
platform = DevicePlatform.Huawei
|
||||
users = hashSetOf(UserEntity())
|
||||
}
|
||||
|
||||
private val deviceWithUser = DeviceEntity().apply {
|
||||
id = UUID.fromString("193061cd-b121-414f-952f-426eea3d9be2")
|
||||
platform = DevicePlatform.Apple
|
||||
users = hashSetOf(UserEntity().apply {
|
||||
anonymous = false
|
||||
})
|
||||
}
|
||||
|
||||
private val defaultUserId = UUID.fromString("0daacbb9-74cd-4c98-bdbd-584dfcf5d342")
|
||||
|
||||
@BeforeEach
|
||||
fun prepareMocks() {
|
||||
doReturn(
|
||||
deviceWithoutUser
|
||||
).`when`(deviceRepository).findByIdWithLock(deviceWithoutUser.id!!)
|
||||
|
||||
doReturn(
|
||||
deviceWithAnonymousUser
|
||||
).`when`(deviceRepository).findByIdWithLock(deviceWithAnonymousUser.id!!)
|
||||
|
||||
doReturn(
|
||||
deviceWithUser
|
||||
).`when`(deviceRepository).findByIdWithLock(deviceWithUser.id!!)
|
||||
|
||||
doAnswer { invocation ->
|
||||
(invocation.getArgument(0) as UserEntity)
|
||||
.apply {
|
||||
id = defaultUserId
|
||||
}
|
||||
}.`when`(userRepository).save(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должен создаваться анонимный пользователь")
|
||||
fun anonymousUserShouldBeCreated() {
|
||||
val actualUser = userCoreService.create(
|
||||
NewAnonymousUser(
|
||||
deviceId = deviceWithoutUser.id!!
|
||||
)
|
||||
)
|
||||
|
||||
val expectedUser = User(
|
||||
id = defaultUserId,
|
||||
device = deviceWithoutUser.toDto(),
|
||||
scopes = emptySet()
|
||||
)
|
||||
|
||||
assertEquals(expectedUser, actualUser)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Новый анонимный пользователь не может быть привязан к уже занятому устройству")
|
||||
fun shouldBeDeviceAlreadyLinkedException() {
|
||||
assertThrows<DeviceAlreadyLinkedException> {
|
||||
userCoreService.create(
|
||||
NewAnonymousUser(
|
||||
deviceId = deviceWithAnonymousUser.id!!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("У анонимного пользователя должен быть флаг anonymous=true и confirmed=false")
|
||||
fun shouldBeAnonymousFlags() {
|
||||
userCoreService.create(
|
||||
NewAnonymousUser(
|
||||
deviceId = deviceWithoutUser.id!!,
|
||||
)
|
||||
)
|
||||
|
||||
verify(userRepository).save(argThat { anonymous })
|
||||
verify(userRepository).save(argThat { confirmedAt == null })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("У обычного пользователя должен быть флаг anonymous=false и confirmed=false")
|
||||
fun shouldNotBeAnonymousFlags() {
|
||||
userCoreService.create(
|
||||
NewUser(
|
||||
deviceId = deviceWithoutUser.id!!,
|
||||
username = "user",
|
||||
password = null,
|
||||
identifierType = IdentifierType.Username,
|
||||
)
|
||||
)
|
||||
|
||||
verify(userRepository).save(argThat { !anonymous })
|
||||
verify(userRepository).save(argThat { confirmedAt == null })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должен создаваться обычный пользователь")
|
||||
fun userShouldBeCreated() {
|
||||
doReturn(
|
||||
listOf(
|
||||
ScopeEntity().apply {
|
||||
name = "app:api"
|
||||
}
|
||||
)
|
||||
).`when`(scopeRepository).findByGroup(any())
|
||||
|
||||
val actualUser = userCoreService.create(
|
||||
NewUser(
|
||||
deviceId = deviceWithoutUser.id!!,
|
||||
username = "admin",
|
||||
password = "foo",
|
||||
identifierType = IdentifierType.PhoneNumber,
|
||||
)
|
||||
)
|
||||
|
||||
val expectedUser = User(
|
||||
id = defaultUserId,
|
||||
device = deviceWithoutUser.toDto(),
|
||||
scopes = setOf(Scope("app:api"))
|
||||
)
|
||||
|
||||
assertEquals(expectedUser, actualUser)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
spring:
|
||||
config:
|
||||
import: "test-slow.yml"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
spring:
|
||||
config:
|
||||
import: "test.yml"
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
databaseChangeLog:
|
||||
- changeset:
|
||||
id: 1
|
||||
author: test
|
||||
- includeAll:
|
||||
path: classpath*:/db/changelog/auth/core
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
plugins {
|
||||
id("kotlin")
|
||||
id("kotlin-spring")
|
||||
kotlin("plugin.jpa")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":auth-core"))
|
||||
|
||||
implementation("com.auth0:java-jwt")
|
||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.auth.core.configurations
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Import
|
||||
|
||||
@Configuration
|
||||
@Import(AuthCoreConfiguration::class)
|
||||
@ComponentScan("ru.touchin.auth.core.tokens")
|
||||
@ConfigurationPropertiesScan("ru.touchin.auth.core.tokens")
|
||||
class AuthTokenConfiguration
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.auth.core.configurations
|
||||
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Import
|
||||
|
||||
@Configuration
|
||||
@Import(AuthCoreDatabaseConfiguration::class)
|
||||
@EntityScan("ru.touchin.auth.core.tokens")
|
||||
class AuthTokenDatabaseConfiguration
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package ru.touchin.auth.core.tokens.access.config
|
||||
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import ru.touchin.auth.core.tokens.access.properties.AccessTokenProperties
|
||||
import ru.touchin.common.string.StringUtils.emptyString
|
||||
import java.security.KeyFactory
|
||||
import java.security.interfaces.RSAPrivateKey
|
||||
import java.security.interfaces.RSAPublicKey
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import java.util.*
|
||||
|
||||
@Configuration
|
||||
class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProperties) {
|
||||
|
||||
@Bean
|
||||
fun accessTokenSigningAlgorithm(): Algorithm {
|
||||
return Algorithm.RSA256(
|
||||
accessTokenPublicKey(),
|
||||
accessTokenPrivateKey()
|
||||
)
|
||||
}
|
||||
|
||||
@Bean("accessTokenPublicKey")
|
||||
fun accessTokenPublicKey(): RSAPublicKey {
|
||||
val keySpecX509 = getKeySpec(accessTokenProperties.keyPair.public, ::X509EncodedKeySpec)
|
||||
|
||||
return keyFactory.generatePublic(keySpecX509) as RSAPublicKey
|
||||
}
|
||||
|
||||
@Bean("accessTokenPrivateKey")
|
||||
fun accessTokenPrivateKey(): RSAPrivateKey {
|
||||
val keySpecPKCS8 = getKeySpec(accessTokenProperties.keyPair.private, ::PKCS8EncodedKeySpec)
|
||||
|
||||
return keyFactory.generatePrivate(keySpecPKCS8) as RSAPrivateKey
|
||||
}
|
||||
|
||||
private fun <T> getKeySpec(key: String, keySpecFn: (ByteArray) -> T): T {
|
||||
val rawKey = getRawKey(key)
|
||||
|
||||
return Base64.getDecoder()
|
||||
.decode(rawKey)
|
||||
.let(keySpecFn)
|
||||
}
|
||||
|
||||
private fun getRawKey(key: String): String {
|
||||
return key
|
||||
.replace("-----BEGIN .+KEY-----".toRegex(), emptyString())
|
||||
.replace("-----END .+KEY-----".toRegex(), emptyString())
|
||||
.replace("\n", emptyString())
|
||||
.trim()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.auth.core.tokens.access.dto
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
data class AccessToken(
|
||||
val value: String,
|
||||
val timeToLive: Duration
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.auth.core.tokens.access.dto
|
||||
|
||||
data class AccessTokenRequest(
|
||||
val subject: String,
|
||||
val claims: List<Pair<String, Any>>,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.auth.core.tokens.access.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class AccessTokenMalformedException(description: String?) : CommonException(description)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.auth.core.tokens.access.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class UnsupportedClaimType(typeName: String?) : CommonException("Unsupported claim type: $typeName")
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package ru.touchin.auth.core.tokens.access.properties
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.ConstructorBinding
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm
|
||||
import java.time.Duration
|
||||
|
||||
data class AccessTokenKeyPair(val public: String, val private: String)
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "token.access")
|
||||
data class AccessTokenProperties(
|
||||
val keyPair: AccessTokenKeyPair,
|
||||
val issuer: String,
|
||||
val timeToLive: Duration,
|
||||
val signatureAlgorithm: SignatureAlgorithm
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.auth.core.tokens.access.services
|
||||
|
||||
import ru.touchin.auth.core.tokens.access.dto.AccessToken
|
||||
import ru.touchin.auth.core.tokens.access.dto.AccessTokenRequest
|
||||
|
||||
interface AccessTokenService {
|
||||
|
||||
fun create(accessTokenRequest: AccessTokenRequest): AccessToken
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package ru.touchin.auth.core.tokens.access.services
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.JWTCreator
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import org.springframework.stereotype.Service
|
||||
import ru.touchin.auth.core.tokens.access.dto.AccessToken
|
||||
import ru.touchin.auth.core.tokens.access.dto.AccessTokenRequest
|
||||
import ru.touchin.auth.core.tokens.access.exceptions.UnsupportedClaimType
|
||||
import ru.touchin.auth.core.tokens.access.properties.AccessTokenProperties
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
|
||||
@Service
|
||||
class JwtAccessTokenServiceImpl(
|
||||
private val accessTokenProperties: AccessTokenProperties,
|
||||
private val accessTokenSigningAlgorithm: Algorithm
|
||||
) : AccessTokenService {
|
||||
|
||||
private fun sign(builder: JWTCreator.Builder) = builder.sign(accessTokenSigningAlgorithm)
|
||||
|
||||
private fun getExpirationDate(): Date {
|
||||
return LocalDateTime.now()
|
||||
.plus(accessTokenProperties.timeToLive)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.let(Date::from)
|
||||
}
|
||||
|
||||
override fun create(accessTokenRequest: AccessTokenRequest): AccessToken {
|
||||
val token = JWT.create()
|
||||
.withIssuer(accessTokenProperties.issuer)
|
||||
.withExpiresAt(getExpirationDate())
|
||||
.withSubject(accessTokenRequest.subject)
|
||||
.apply {
|
||||
accessTokenRequest.claims.forEach { (name, value) ->
|
||||
when(value) {
|
||||
is String -> withClaim(name, value)
|
||||
is Int -> withClaim(name, value)
|
||||
else -> throw UnsupportedClaimType(value::class.simpleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
.let(this::sign)
|
||||
|
||||
return AccessToken(token, accessTokenProperties.timeToLive)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.dto
|
||||
|
||||
import ru.touchin.auth.core.user.dto.User
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
data class RefreshToken(
|
||||
val value: String,
|
||||
val expiresAt: ZonedDateTime,
|
||||
val user: User,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class RefreshTokenExpiredException(value: String) : CommonException(
|
||||
"Refresh token with value $value is expired"
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonNotFoundException
|
||||
|
||||
class RefreshTokenNotFoundException(value: String) : CommonNotFoundException(
|
||||
"No token found with value $value"
|
||||
)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.models
|
||||
|
||||
import ru.touchin.auth.core.device.models.DeviceEntity
|
||||
import ru.touchin.auth.core.scope.models.ScopeEntity
|
||||
import ru.touchin.auth.core.tokens.refresh.exceptions.RefreshTokenExpiredException
|
||||
import ru.touchin.auth.core.user.models.UserEntity
|
||||
import ru.touchin.common.date.DateUtils.isExpired
|
||||
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
|
||||
import java.time.ZonedDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.JoinTable
|
||||
import javax.persistence.ManyToMany
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "refresh_tokens")
|
||||
class RefreshTokenEntity : AuditableUuidIdEntity() {
|
||||
|
||||
lateinit var value: String
|
||||
|
||||
lateinit var expiresAt: ZonedDateTime
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
lateinit var user: UserEntity
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "device_id")
|
||||
var device: DeviceEntity? = null
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "refresh_token_scopes",
|
||||
joinColumns = [JoinColumn(name = "refresh_token_id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "scope_name")]
|
||||
)
|
||||
lateinit var scopes: Set<ScopeEntity>
|
||||
|
||||
fun validate(): RefreshTokenEntity = this.apply {
|
||||
if (expiresAt.isExpired()) {
|
||||
throw RefreshTokenExpiredException(value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.properties
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.ConstructorBinding
|
||||
import java.time.Duration
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "token.refresh")
|
||||
data class RefreshTokenProperties(
|
||||
val length: Int,
|
||||
val prefix: String,
|
||||
val timeToLive: Duration
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.repositories
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import ru.touchin.auth.core.tokens.refresh.exceptions.RefreshTokenNotFoundException
|
||||
import ru.touchin.auth.core.tokens.refresh.models.RefreshTokenEntity
|
||||
|
||||
interface RefreshTokenRepository : JpaRepository<RefreshTokenEntity, Long> {
|
||||
|
||||
fun findByValue(value: String): RefreshTokenEntity?
|
||||
|
||||
}
|
||||
|
||||
fun RefreshTokenRepository.findByValueOrThrow(value: String): RefreshTokenEntity {
|
||||
return findByValue(value)
|
||||
?: throw RefreshTokenNotFoundException(value)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.services
|
||||
|
||||
import ru.touchin.auth.core.tokens.refresh.dto.RefreshToken
|
||||
import ru.touchin.auth.core.tokens.refresh.services.dto.NewRefreshToken
|
||||
|
||||
interface RefreshTokenService {
|
||||
|
||||
fun get(value: String): RefreshToken
|
||||
fun create(token: NewRefreshToken): RefreshToken
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.services
|
||||
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
|
||||
import ru.touchin.auth.core.device.repository.DeviceRepository
|
||||
import ru.touchin.auth.core.scope.dto.Scope
|
||||
import ru.touchin.auth.core.scope.repositories.ScopeRepository
|
||||
import ru.touchin.auth.core.user.repositories.UserRepository
|
||||
import ru.touchin.auth.core.user.repositories.findByIdOrThrow
|
||||
import ru.touchin.auth.core.tokens.refresh.dto.RefreshToken
|
||||
import ru.touchin.auth.core.tokens.refresh.models.RefreshTokenEntity
|
||||
import ru.touchin.auth.core.tokens.refresh.properties.RefreshTokenProperties
|
||||
import ru.touchin.auth.core.tokens.refresh.repositories.RefreshTokenRepository
|
||||
import ru.touchin.auth.core.tokens.refresh.repositories.findByValueOrThrow
|
||||
import ru.touchin.auth.core.tokens.refresh.services.dto.NewRefreshToken
|
||||
import ru.touchin.auth.core.user.converters.UserConverter.toDto
|
||||
import ru.touchin.common.random.SecureRandomStringGenerator
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Service
|
||||
class RefreshTokenServiceImpl(
|
||||
private val refreshTokenProperties: RefreshTokenProperties,
|
||||
private val refreshTokenRepository: RefreshTokenRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val deviceRepository: DeviceRepository,
|
||||
private val scopeRepository: ScopeRepository,
|
||||
) : RefreshTokenService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun get(value: String): RefreshToken {
|
||||
return refreshTokenRepository.findByValueOrThrow(value)
|
||||
.toDto()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun create(token: NewRefreshToken): RefreshToken {
|
||||
val user = userRepository.findByIdOrThrow(token.userId)
|
||||
val device = token.deviceId?.let(deviceRepository::findByIdOrNull)
|
||||
val scopes = scopeRepository.findAllById(token.scopes.map(Scope::name))
|
||||
|
||||
val model = RefreshTokenEntity().apply {
|
||||
expiresAt = getExpirationDate()
|
||||
value = generateTokenValue()
|
||||
this.user = user
|
||||
this.device = device
|
||||
this.scopes = scopes.toSet()
|
||||
}
|
||||
|
||||
return refreshTokenRepository.save(model)
|
||||
.toDto()
|
||||
}
|
||||
|
||||
private fun getExpirationDate(): ZonedDateTime {
|
||||
return ZonedDateTime.now().plus(refreshTokenProperties.timeToLive)
|
||||
}
|
||||
|
||||
private fun generateTokenValue(): String {
|
||||
return refreshTokenProperties.let {
|
||||
it.prefix + SecureRandomStringGenerator.generate(it.length)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun RefreshTokenEntity.toDto(): RefreshToken {
|
||||
val device = device?.toDto()
|
||||
|
||||
return RefreshToken(
|
||||
value = value,
|
||||
expiresAt = expiresAt,
|
||||
user = user.toDto(device)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.auth.core.tokens.refresh.services.dto
|
||||
|
||||
import ru.touchin.auth.core.scope.dto.Scope
|
||||
import java.util.*
|
||||
|
||||
data class NewRefreshToken(
|
||||
val userId: UUID,
|
||||
val deviceId: UUID?,
|
||||
val scopes: Set<Scope>
|
||||
)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105201851__create_table__refresh_tokens
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: refresh_tokens
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: refresh_tokens
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: pk_refresh_tokens
|
||||
- column:
|
||||
name: user_id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
references: users(id)
|
||||
foreignKeyName: fk_refresh_tokens_user_id
|
||||
- column:
|
||||
name: device_id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: true
|
||||
references: devices(id)
|
||||
foreignKeyName: fk_refresh_tokens_device_id
|
||||
- column:
|
||||
name: value
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
unique: true
|
||||
nullable: false
|
||||
- column:
|
||||
name: expires_at
|
||||
type: TIMESTAMP WITH TIME ZONE
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: created_by
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: updated_by
|
||||
type: VARCHAR(255)
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
defaultValueDate: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
- createIndex:
|
||||
indexName: idx_refresh_tokens_user_id
|
||||
tableName: refresh_tokens
|
||||
columns:
|
||||
- column:
|
||||
name: user_id
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202105201901__create_table__refresh_token_scopes
|
||||
author: touchin
|
||||
preConditions:
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: refresh_token_scopes
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: refresh_token_scopes
|
||||
columns:
|
||||
- column:
|
||||
name: refresh_token_id
|
||||
type: UUID
|
||||
constraints:
|
||||
nullable: false
|
||||
references: refresh_tokens(id)
|
||||
foreignKeyName: fk_refresh_token_scopes_refresh_token_id
|
||||
- column:
|
||||
name: scope_name
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
references: scopes(name)
|
||||
foreignKeyName: fk_refresh_token_scopes_scope_name
|
||||
- addPrimaryKey:
|
||||
tableName: refresh_token_scopes
|
||||
columnNames: refresh_token_id, scope_name
|
||||
constraintName: pk_refresh_token_scopes
|
||||
|
|
@ -66,6 +66,8 @@ subprojects {
|
|||
dependency("tech.units:indriya:2.1.2")
|
||||
|
||||
dependency("org.locationtech.spatial4j:spatial4j:0.8")
|
||||
|
||||
dependency("com.auth0:java-jwt:3.10.3")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,3 +39,5 @@ include("exception-handler-logger-spring-web")
|
|||
include("version-spring-web")
|
||||
include("response-wrapper-spring-web")
|
||||
include("settings-spring-jpa")
|
||||
include("auth-core")
|
||||
include("auth-jwt-core")
|
||||
|
|
|
|||
Loading…
Reference in New Issue