add auth module

This commit is contained in:
Vasili Karaev 2021-06-14 10:51:05 +03:00
parent 2265170de3
commit 61db7e831f
82 changed files with 2521 additions and 0 deletions

View File

@ -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
```

View File

@ -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")
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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,
)
}
}

View File

@ -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,
)

View File

@ -0,0 +1,5 @@
package ru.touchin.auth.core.device.dto.enums
enum class DevicePlatform {
Android, Huawei, Apple
}

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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
}
}
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -0,0 +1,5 @@
package ru.touchin.auth.core.policy.dto
data class RegistrationPolicy(
val multiAccountsPerDevice: Boolean
)

View File

@ -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
)
}
}

View File

@ -0,0 +1,9 @@
package ru.touchin.auth.core.policy.services
import ru.touchin.auth.core.policy.dto.RegistrationPolicy
interface PolicyService {
fun getRegistrationPolicy(): RegistrationPolicy
}

View File

@ -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)
}
}

View File

@ -0,0 +1,5 @@
package ru.touchin.auth.core.scope.dto
data class Scope(
val name: String
)

View File

@ -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"
)

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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)
}

View File

@ -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()
)
}
}

View File

@ -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>,
)

View File

@ -0,0 +1,5 @@
package ru.touchin.auth.core.user.dto.enums
enum class IdentifierType {
Username, PhoneNumber, Email
}

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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'"
)

View File

@ -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
}

View File

@ -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>
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()
})
}
}

View File

@ -0,0 +1,7 @@
package ru.touchin.auth.core.user.services.dto
import java.util.*
data class NewAnonymousUser(
val deviceId: UUID
)

View File

@ -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?,
)

View File

@ -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?,
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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!!)
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,3 @@
spring:
config:
import: "test-slow.yml"

View File

@ -0,0 +1,4 @@
spring:
config:
import: "test.yml"

View File

@ -0,0 +1,6 @@
databaseChangeLog:
- changeset:
id: 1
author: test
- includeAll:
path: classpath*:/db/changelog/auth/core

View File

@ -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")
}

View File

@ -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

View File

@ -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

View File

@ -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")
}
}

View File

@ -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
)

View File

@ -0,0 +1,6 @@
package ru.touchin.auth.core.tokens.access.dto
data class AccessTokenRequest(
val subject: String,
val claims: List<Pair<String, Any>>,
)

View File

@ -0,0 +1,5 @@
package ru.touchin.auth.core.tokens.access.exceptions
import ru.touchin.common.exceptions.CommonException
class AccessTokenMalformedException(description: String?) : CommonException(description)

View File

@ -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")

View File

@ -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
)

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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,
)

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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)
}
}
}

View File

@ -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
)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
)
}
}
}

View File

@ -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>
)

View File

@ -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

View File

@ -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

View File

@ -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")
}
}

View File

@ -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")