Compare commits

..

No commits in common. "master" and "feature/auth" have entirely different histories.

391 changed files with 229 additions and 9783 deletions

328
README.md
View File

@ -20,19 +20,6 @@
} }
``` ```
## Project Gradle tasks
### Detekt:
- `gradle $project:detekt` - detect not formatted, complex code.
Reports are stored in "$pwd/build/reports/kotlin-detekt-${project.name}.html".
### DiKTat:
- `gradle :diktatCheck` - detect not formatted code of "kt", "kts" files;
- `gradle :diktatFix` - if possible, fix not formatted code. Known issues: full fix may require 1+ launch in order to apply all rules; some rules potentially may break code syntax.
By setting environment variable `TASKS_FILE_REPORT_ENABLED`(true, false) you may configure raw console output or html file as report.
Reports are stored in "$pwd/build/reports/diktat-report.html".
## common ## common
Набор утилит, структур данных, исключений без привязки к `spring` Набор утилит, структур данных, исключений без привязки к `spring`
@ -81,106 +68,6 @@ Reports are stored in "$pwd/build/reports/diktat-report.html".
Утилиты для тестирования репозиториев Утилиты для тестирования репозиториев
## codestyle-archunit
Набор правил для поддержки оформления архитектуры.
#### Список доступных правил
- `ru.touchin.codestyle.archunit.rules.ClassNamingArchRules`
- `ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules`
#### Gradle plugin
Настройка и применение совместно с [ArchUnit Gradle Plugin](https://github.com/societe-generale/arch-unit-gradle-plugin).
Действие `checkRules` для проверки соответствие правилам запускается при операциях сборки, по умолчанию.
Вручную можно вызвать командой ``gradle :checkRules`` для нужного модуля.
Добавить его можно следующим образом на примере установки в рутовый gradle.build проекта:
Groovy DSL:
```groovy
buildscript {
dependencies {
classpath "com.societegenerale.commons:arch-unit-gradle-plugin:3.0.0"
}
}
subprojects {
dependencyManagement {
dependencies {
dependency "com.tngtech.archunit:archunit:1.0.1"
}
}
apply plugin: "java"
apply plugin: "com.societegenerale.commons.arch-unit-gradle-plugin"
archUnit {
mainScopePath = "/classes/kotlin/main" // or "/classes/java/main"
testScopePath = "/classes/kotlin/test" // or "/classes/java/test"
var applyType = applyOn("ru.touchin", "main")
configurableRules = [
configurableRule(
"ru.touchin.codestyle.archunit.rules.ClassNamingArchRules",
applyType,
),
configurableRule(
"ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules",
applyType,
),
]
}
dependencies {
archUnitExtraLib "ru.touchin:codestyle-archunit" // or archUnitExtraLib project(":codestyle-archunit")
}
}
```
Kotlin DSL:
```kotlin
plugins {
id("com.societegenerale.commons.arch-unit-gradle-plugin") version "3.0.0"
}
subprojects {
configure<DependencyManagementExtension> {
dependencies {
dependency("com.tngtech.archunit:archunit:1.0.1")
}
}
apply(plugin = "java")
apply(plugin = "com.societegenerale.commons.arch-unit-gradle-plugin")
archUnit {
mainScopePath = "/classes/kotlin/main" // or "/classes/java/main"
testScopePath = "/classes/kotlin/test" // or "/classes/java/test"
configurableRules = listOf(
"ru.touchin.codestyle.archunit.rules.ClassNamingArchRules",
"ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules"
).map { package ->
configurableRule(
package,
applyOn("ru.touchin", "main")
)
}
}
dependencies {
archUnitExtraLib("ru.touchin:codestyle-archunit") // or archUnitExtraLib(project(":codestyle-archunit"))
}
}
```
Отключить проверки на таске помимо конфигурирования `configurableRule` можно также таким образом:
```kotlin
// clear action launch for root project to avoid exception
tasks.checkRules.configure {
actions.clear()
}
```
## logger ## logger
Основные компоненты логирования: Основные компоненты логирования:
@ -202,17 +89,11 @@ Interceptor для логирования запросов/ответов.
## exception-handler-spring-web ## exception-handler-spring-web
Перехватывает ошибки сервера, определяет код ошибки и возвращает их в правильный `response`. Перехватывает ошибки сервера, определяет код ошибки и возвращает их в правильный `response`
Подключается с помощью аннотации `@EnableSpringExceptionHandler`
## exception-handler-logger-spring-web ## exception-handler-logger-spring-web
Добавляет логирование в обработку ошибок. Добавляет логирование в обработку ошибок
Подключается с помощью аннотации `@EnableSpringExceptionHandlerLogger` до подключения основного модуля.
## validation-spring
Добавляет аннотации для валидации запросов.
## version-spring-web ## version-spring-web
@ -242,11 +123,11 @@ Interceptor для логирования запросов/ответов.
Модуль для хранения настроек Модуль для хранения настроек
## security-authorization-server-core ## auth-core
Модуль авторизации Модуль авторизации
## security-authorization-server-jwt-core ## auth-jwt-core
Добавляет поддержку jwt-токенов (создание/хранение). Для работы этого модуля требуется прописать в пропертях: Добавляет поддержку jwt-токенов (создание/хранение). Для работы этого модуля требуется прописать в пропертях:
@ -267,204 +148,3 @@ token.refresh:
prefix: RT- prefix: RT-
timeToLive: PT2H # 2 hours timeToLive: PT2H # 2 hours
``` ```
Генерация ключей:
```bash
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -pubout -out public.pem
openssl pkcs8 -topk8 -inform PEM -in private.pem -out private_key.pem -nocrypt
cat private_key.pem
cat public.pem
```
## security-authorization-server-oauth2-metadata
OAuth2 metadata support.
## security-jwt-common
JWT related utilities.
## security-resource-server-default-configuration
Default configuration for the Spring OAuth2 resource server with JWT auth.
## security-resource-server-custom-configuration
Custom configuration for the Spring OAuth2 resource server with JWT auth. Requires the following properties:
``` yaml
token.access:
issuer: ${app.issuer}
signatureAlgorithm: RS256
keyPair:
public: |
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
```
## security-resource-server-test-configuration
Disables Spring OAuth2 resource server for testing.
## s3-storage
Amazon S3 support.
## server-info-spring-web
Allow include headers with information about the server in responses
To get started you need:
1) Add annotation to configuration
2) Add property to yml/properties file:
```
server.info:
buildVersion: ${buildVersion}
```
3) Implement ServerInfoService (optional. If you want to add other headers)
4) Add dir with impl ServerInfoService in ComponentScan annotation
## push-message-provider
Интерфейсы и компоненты для модулей по обеспечению интеграции с сервисами отправки пуш-уведомлений. Является необходимой зависимостью для использования провайдеров.
Далее рассматривается пример использования подключаемых модулей-провайдеров.
``` kotlin
@Service
class PushSendingService(
private val pushMessageProviderServiceFactory: PushMessageProviderServiceFactory
) {
fun sendPushMessage() {
val yourPushToken = "pushTokenForChecking"
val platform = PlatformType.ANDROID_GOOGLE
val pushMessageProvider: PushMessageProviderService = pushMessageProviderServiceFactory.get(platform)
val result = pushMessageProvider.check( // Проверка валидности токена для обозначения целесообразности отправки
PushTokenCheck(
pushToken = yourPushToken
)
)
if (result.status == PushTokenStatus.VALID) { // Токен валиден, PushMessageProviderService интегрирован в систему
// Отправка пуш-уведомления
pushMessageProvider.send(
PushTokenMessage(
token = yourPushToken,
pushMessageNotification = PushMessageNotification(
title = "Your PushMessage",
description = "Provided by PushMessageProviderService",
imageUrl = null
),
data = mapOf(
"customKey" to "customData"
)
)
)
}
}
}
```
## push-message-provider-fcm
Модуль по обеспечению интеграции с Firebase Cloud Messaging.
1) Подключение компонентов Spring осуществляется при помощи аннотации `@EnablePushMessageProviderFcm`.
2) Необходимо добавление конфигурации для модуля с выбранным способом хранения данных для авторизации. Пример файла конфигурации в формате yaml:
``` yaml
push-message-provider:
platformProviders:
ANDROID_GOOGLE:
- FCM
IOS:
- FCM
fcm:
appName: yourAppName
auth:
# Выбранный тип авторизации
client:
readTimeout: 10s
connectionTimeout: 1s
```
3) Настраивается способ предоставления авторизации для Firebase Cloud Messaging.
А) Токен доступа из консоли Google, добавляемый в конфигурацию настроек:
``` yaml
auth:
token:
value: testValue
expiresAt: 2023-01-01 23:59:59 +00:00
```
B) Данные в файле из консоли Firebase, добавляемые в resources с обозначением пути в конфигурации настроек:
``` yaml
auth:
credentialsFile:
path: credentials/firebase-admin.json
```
C) Данные из файла консоли Firebase, добавляемые в конфигурацию настроек:
``` yaml
auth:
credentialsData:
type: service_account
projectId: yourProjectId
privateKeyId: yourPrivateKeyId
privateKey: |
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
clientEmail: yourClientEmail
clientId: yourClientId
authUri: yourAuthUri
tokenUri: yourTokenUri
authProviderX509CertUrl: yourAuthProviderX509CertUrl
clientX509CertUrl: yourClientX509CertUrl
```
## push-message-provider-hpk
Модуль по обеспечению интеграции с Huawei Push Kit.
1) Подключение нового провайдера осуществляется при помощи аннотации `@EnablePushMessageProviderHpk`.
2) Для логирования запросов к сервису HPK нужно встроить в контейнер Spring собственный `WebClientLogger` из модуля `logger-spring-web` или же использовать стандартный посредством импорта конфигурации:
``` kotlin
@Import(
SpringLoggerConfiguration::class,
SpringLoggerWebConfiguration::class
)
class YourConfiguration
```
3) Нужно добавить конфигурацию для считывания модулем. Пример файла в формате yaml:
``` yaml
push-message-provider:
platformProviders:
ANDROID_HUAWEI:
- HPK
hpk:
web-services:
client-id: yourClientId
oauth:
client-secret: yourClientSecret
url: https://oauth-login.cloud.huawei.com/oauth2/v3/
http:
connection-timeout: 1s
read-timeout: 10s
write-timeout: 10s
ssl: # Опциональная структура
handshake-timeout: 1s
notify-read-timeout: 1s
notify-flush-timeout: 1s
hpk:
url: https://push-api.cloud.huawei.com/v1/
http:
connection-timeout: 1s
read-timeout: 10s
write-timeout: 10s
ssl: # Опциональная структура
handshake-timeout: 1s
notify-read-timeout: 1s
notify-flush-timeout: 1s
```

View File

@ -8,7 +8,6 @@ dependencies {
runtimeOnly("org.postgresql:postgresql") runtimeOnly("org.postgresql:postgresql")
api(project(":common")) api(project(":common"))
api(project(":common-device"))
api(project(":common-spring-jpa")) api(project(":common-spring-jpa"))

View File

@ -8,7 +8,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
@ComponentScan("ru.touchin.auth.core") @ComponentScan("ru.touchin.auth.core")
@ConfigurationPropertiesScan("ru.touchin.auth.core") @ConfigurationPropertiesScan
class AuthCoreConfiguration { class AuthCoreConfiguration {
@Bean @Bean

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

@ -1,6 +1,6 @@
package ru.touchin.auth.core.device.dto package ru.touchin.auth.core.device.dto
import ru.touchin.common.devices.enums.DevicePlatform import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import java.util.* import java.util.*
data class Device( data class Device(

View File

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

View File

@ -1,8 +1,7 @@
package ru.touchin.auth.core.device.models package ru.touchin.auth.core.device.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import ru.touchin.auth.core.user.models.UserEntity import ru.touchin.auth.core.user.models.UserEntity
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
import javax.persistence.Entity import javax.persistence.Entity
import javax.persistence.JoinColumn import javax.persistence.JoinColumn
@ -11,7 +10,7 @@ import javax.persistence.ManyToMany
import javax.persistence.Table import javax.persistence.Table
@Entity @Entity
@Table(name = "devices", schema = SCHEMA) @Table(name = "devices")
class DeviceEntity: AuditableUuidIdEntity() { class DeviceEntity: AuditableUuidIdEntity() {
lateinit var platform: DevicePlatform lateinit var platform: DevicePlatform
@ -19,26 +18,11 @@ class DeviceEntity: AuditableUuidIdEntity() {
@ManyToMany @ManyToMany
@JoinTable( @JoinTable(
name = "devices_users", name = "devices_users",
schema = SCHEMA,
joinColumns = [JoinColumn(name = "device_id")], joinColumns = [JoinColumn(name = "device_id")],
inverseJoinColumns = [JoinColumn(name = "user_id")] inverseJoinColumns = [JoinColumn(name = "user_id")]
) )
lateinit var users: Set<UserEntity> lateinit var users: Set<UserEntity>
override fun equals(other: Any?): Boolean {
if (other == null || this.javaClass != other.javaClass) {
return false
}
other as DeviceEntity
return this.id == other.id
}
override fun hashCode(): Int {
return this.id.hashCode()
}
companion object { companion object {
fun create(platform: DevicePlatform): DeviceEntity { fun create(platform: DevicePlatform): DeviceEntity {
return DeviceEntity().apply { return DeviceEntity().apply {

View File

@ -1,7 +1,7 @@
package ru.touchin.auth.core.device.services package ru.touchin.auth.core.device.services
import ru.touchin.auth.core.device.dto.Device import ru.touchin.auth.core.device.dto.Device
import ru.touchin.common.devices.enums.DevicePlatform import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import java.util.UUID import java.util.UUID
interface DeviceCoreService { interface DeviceCoreService {

View File

@ -6,9 +6,9 @@ import org.springframework.transaction.annotation.Transactional
import ru.touchin.auth.core.device.converters.DeviceConverter.toDto import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
import ru.touchin.auth.core.device.dto.Device import ru.touchin.auth.core.device.dto.Device
import ru.touchin.auth.core.device.models.DeviceEntity 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.DeviceRepository
import ru.touchin.auth.core.device.repository.findByIdOrThrow import ru.touchin.auth.core.device.repository.findByIdOrThrow
import ru.touchin.common.devices.enums.DevicePlatform
import java.util.* import java.util.*
@Service @Service

View File

@ -1,5 +1,6 @@
package ru.touchin.auth.core.policy.services package ru.touchin.auth.core.policy.services
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import ru.touchin.auth.core.policy.dto.RegistrationPolicy import ru.touchin.auth.core.policy.dto.RegistrationPolicy

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

@ -1,8 +1,5 @@
@file:Suppress("unused")
package ru.touchin.auth.core.scope.models package ru.touchin.auth.core.scope.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.common.spring.jpa.models.BaseUuidIdEntity import ru.touchin.common.spring.jpa.models.BaseUuidIdEntity
import javax.persistence.Entity import javax.persistence.Entity
import javax.persistence.JoinColumn import javax.persistence.JoinColumn
@ -10,7 +7,7 @@ import javax.persistence.ManyToOne
import javax.persistence.Table import javax.persistence.Table
@Entity @Entity
@Table(name = "scope_groups", schema = SCHEMA) @Table(name = "scope_groups")
class ScopeGroupEntity : BaseUuidIdEntity() { class ScopeGroupEntity : BaseUuidIdEntity() {
lateinit var groupName: String lateinit var groupName: String

View File

@ -1,8 +1,8 @@
package ru.touchin.auth.core.user.models package ru.touchin.auth.core.user.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.auth.core.user.dto.enums.IdentifierType import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
import java.time.ZonedDateTime
import javax.persistence.Entity import javax.persistence.Entity
import javax.persistence.EnumType import javax.persistence.EnumType
import javax.persistence.Enumerated import javax.persistence.Enumerated
@ -11,7 +11,7 @@ import javax.persistence.ManyToOne
import javax.persistence.Table import javax.persistence.Table
@Entity @Entity
@Table(name = "user_accounts", schema = SCHEMA) @Table(name = "user_accounts")
class UserAccountEntity: AuditableUuidIdEntity() { class UserAccountEntity: AuditableUuidIdEntity() {
lateinit var username: String lateinit var username: String

View File

@ -1,6 +1,5 @@
package ru.touchin.auth.core.user.models package ru.touchin.auth.core.user.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.auth.core.device.models.DeviceEntity import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.scope.models.ScopeEntity import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
@ -12,7 +11,7 @@ import javax.persistence.ManyToMany
import javax.persistence.Table import javax.persistence.Table
@Entity @Entity
@Table(name = "users", schema = SCHEMA) @Table(name = "users")
class UserEntity: AuditableUuidIdEntity() { class UserEntity: AuditableUuidIdEntity() {
var anonymous: Boolean = true var anonymous: Boolean = true
@ -22,7 +21,6 @@ class UserEntity : AuditableUuidIdEntity() {
@ManyToMany @ManyToMany
@JoinTable( @JoinTable(
name = "devices_users", name = "devices_users",
schema = SCHEMA,
joinColumns = [JoinColumn(name = "user_id")], joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "device_id")] inverseJoinColumns = [JoinColumn(name = "device_id")]
) )
@ -31,14 +29,9 @@ class UserEntity : AuditableUuidIdEntity() {
@ManyToMany @ManyToMany
@JoinTable( @JoinTable(
name = "users_scopes", name = "users_scopes",
schema = SCHEMA,
joinColumns = [JoinColumn(name = "user_id")], joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "scope_name")] inverseJoinColumns = [JoinColumn(name = "scope_name")]
) )
lateinit var scopes: MutableSet<ScopeEntity> lateinit var scopes: Set<ScopeEntity>
fun addScopes(scopes: Collection<ScopeEntity>) {
this.scopes.addAll(scopes)
}
} }

View File

@ -4,7 +4,6 @@ package ru.touchin.auth.core.user.repositories
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.findByIdOrNull
import ru.touchin.auth.core.user.dto.enums.IdentifierType import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException
import ru.touchin.auth.core.user.models.UserAccountEntity import ru.touchin.auth.core.user.models.UserAccountEntity
@ -19,26 +18,9 @@ interface UserAccountRepository: JpaRepository<UserAccountEntity, UUID> {
""") """)
fun findByUsername(username: String, identifierType: IdentifierType): UserAccountEntity? fun findByUsername(username: String, identifierType: IdentifierType): UserAccountEntity?
@Query("""
SELECT ua
FROM UserAccountEntity ua
WHERE ua.user.id = :userId AND identifierType = :identifierType
""")
fun findByUserId(userId: UUID, identifierType: IdentifierType): UserAccountEntity?
}
fun UserAccountRepository.findByIdOrThrow(userAccountId: UUID): UserAccountEntity {
return findByIdOrNull(userAccountId)
?: throw UserAccountNotFoundException(userAccountId.toString())
} }
fun UserAccountRepository.findByUsernameOrThrow(username: String, identifierType: IdentifierType): UserAccountEntity { fun UserAccountRepository.findByUsernameOrThrow(username: String, identifierType: IdentifierType): UserAccountEntity {
return findByUsername(username, identifierType) return findByUsername(username, identifierType)
?: throw UserAccountNotFoundException(username) ?: throw UserAccountNotFoundException(username)
} }
fun UserAccountRepository.findByUserIdOrThrow(userId: UUID, identifierType: IdentifierType): UserAccountEntity {
return findByUserId(userId, identifierType)
?: throw UserAccountNotFoundException(userId.toString())
}

View File

@ -1,28 +1,17 @@
package ru.touchin.auth.core.user.services package ru.touchin.auth.core.user.services
import ru.touchin.auth.core.user.dto.User import ru.touchin.auth.core.user.dto.User
import ru.touchin.auth.core.user.dto.UserAccount
import ru.touchin.auth.core.user.dto.enums.IdentifierType import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.auth.core.user.services.dto.AddUserScopes
import ru.touchin.auth.core.user.services.dto.GetUserAccount
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser 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.NewUser
import ru.touchin.auth.core.user.services.dto.UserLogin import ru.touchin.auth.core.user.services.dto.UserLogin
import ru.touchin.auth.core.user.services.dto.UserLogout
import ru.touchin.auth.core.user.services.dto.UserSetPassword
import ru.touchin.auth.core.user.services.dto.UserUpdatePassword
interface UserCoreService { interface UserCoreService {
fun create(newAnonymousUser: NewAnonymousUser): User fun create(newAnonymousUser: NewAnonymousUser): User
fun create(newUser: NewUser): User fun create(newUser: NewUser): User
fun get(username: String, identifierType: IdentifierType): User fun get(username: String, identifierType: IdentifierType): User
fun getUserAccount(userAccount: GetUserAccount): UserAccount
fun getOrNull(username: String, identifierType: IdentifierType): User? fun getOrNull(username: String, identifierType: IdentifierType): User?
fun login(userLogin: UserLogin): User fun login(userLogin: UserLogin): User
fun logout(userLogout: UserLogout)
fun updatePassword(update: UserUpdatePassword)
fun setPassword(userSetPassword: UserSetPassword)
fun addScopes(addUserScopes: AddUserScopes)
} }

View File

@ -1,8 +1,6 @@
@file:Suppress("unused") @file:Suppress("unused")
package ru.touchin.auth.core.user.services package ru.touchin.auth.core.user.services
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -11,13 +9,10 @@ import ru.touchin.auth.core.device.exceptions.DeviceAlreadyLinkedException
import ru.touchin.auth.core.device.models.DeviceEntity import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.device.repository.DeviceRepository import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow
import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.scope.models.ScopeGroupEntity import ru.touchin.auth.core.scope.models.ScopeGroupEntity
import ru.touchin.auth.core.scope.repositories.ScopeRepository import ru.touchin.auth.core.scope.repositories.ScopeRepository
import ru.touchin.auth.core.user.converters.UserAccountConverter.toDto
import ru.touchin.auth.core.user.converters.UserConverter.toDto import ru.touchin.auth.core.user.converters.UserConverter.toDto
import ru.touchin.auth.core.user.dto.User import ru.touchin.auth.core.user.dto.User
import ru.touchin.auth.core.user.dto.UserAccount
import ru.touchin.auth.core.user.dto.enums.IdentifierType import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException
import ru.touchin.auth.core.user.exceptions.UserAlreadyRegisteredException import ru.touchin.auth.core.user.exceptions.UserAlreadyRegisteredException
@ -26,17 +21,10 @@ import ru.touchin.auth.core.user.models.UserAccountEntity
import ru.touchin.auth.core.user.models.UserEntity import ru.touchin.auth.core.user.models.UserEntity
import ru.touchin.auth.core.user.repositories.UserAccountRepository import ru.touchin.auth.core.user.repositories.UserAccountRepository
import ru.touchin.auth.core.user.repositories.UserRepository import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.auth.core.user.repositories.findByIdOrThrow
import ru.touchin.auth.core.user.repositories.findByUserIdOrThrow
import ru.touchin.auth.core.user.repositories.findByUsernameOrThrow import ru.touchin.auth.core.user.repositories.findByUsernameOrThrow
import ru.touchin.auth.core.user.services.dto.AddUserScopes
import ru.touchin.auth.core.user.services.dto.GetUserAccount
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser 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.NewUser
import ru.touchin.auth.core.user.services.dto.UserLogin import ru.touchin.auth.core.user.services.dto.UserLogin
import ru.touchin.auth.core.user.services.dto.UserLogout
import ru.touchin.auth.core.user.services.dto.UserSetPassword
import ru.touchin.auth.core.user.services.dto.UserUpdatePassword
@Service @Service
class UserCoreServiceImpl( class UserCoreServiceImpl(
@ -58,7 +46,7 @@ class UserCoreServiceImpl(
val user = UserEntity().apply { val user = UserEntity().apply {
anonymous = true anonymous = true
devices = hashSetOf(device) devices = hashSetOf(device)
scopes = mutableSetOf() scopes = emptySet()
} }
return userRepository.save(user) return userRepository.save(user)
@ -80,7 +68,7 @@ class UserCoreServiceImpl(
.apply { .apply {
anonymous = false anonymous = false
devices = hashSetOf(device) devices = hashSetOf(device)
scopes = defaultScopes.toMutableSet() scopes = defaultScopes.toSet()
} }
.also(userRepository::save) .also(userRepository::save)
@ -112,85 +100,19 @@ class UserCoreServiceImpl(
val user = userAccount.user val user = userAccount.user
.apply { .apply {
devices = devices + device devices = hashSetOf(device)
} }
.also(userRepository::save) .also(userRepository::save)
return user.toDto(device.toDto()) return user.toDto(device.toDto())
} }
@Transactional
override fun logout(userLogout: UserLogout) {
val device = deviceRepository.findByIdWithLockOrThrow(userLogout.deviceId)
resetDeviceUsers(device)
userRepository.findByIdOrThrow(userLogout.userId)
.apply {
devices = devices - device
}
.also(userRepository::save)
}
@Transactional
override fun updatePassword(update: UserUpdatePassword) {
val userAccount = userAccountRepository.findByIdOrThrow(update.userAccountId)
if (userAccount.password != null) {
if (!passwordEncoder.matches(update.oldPassword, userAccount.password!!)) {
throw WrongPasswordException("userAccountId=${update.userAccountId}")
}
}
userAccount.apply {
password = update.newPassword?.let(passwordEncoder::encode)
}.also(userAccountRepository::save)
}
@Transactional
override fun setPassword(userSetPassword: UserSetPassword) {
val userAccount = userAccountRepository.findByIdOrThrow(userSetPassword.userAccountId)
userAccount.apply {
password = userSetPassword.newPassword.let(passwordEncoder::encode)
}.also(userAccountRepository::save)
}
@Transactional
override fun addScopes(addUserScopes: AddUserScopes) {
val user = userRepository.findByIdOrThrow(addUserScopes.userId)
val newScopes = addUserScopes.scopes.map { scope ->
scopeRepository.findByIdOrNull(scope)
?: createNewScope(scope)
}
user.addScopes(newScopes)
userRepository.save(user)
}
private fun createNewScope(scope: String): ScopeEntity {
val newScope = ScopeEntity().apply {
name = scope
users = mutableSetOf()
}
return scopeRepository.save(newScope)
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
override fun get(username: String, identifierType: IdentifierType): User { override fun get(username: String, identifierType: IdentifierType): User {
return getOrNull(username, identifierType) return getOrNull(username, identifierType)
?: throw UserAccountNotFoundException(username) ?: throw UserAccountNotFoundException(username)
} }
@Transactional(readOnly = true)
override fun getUserAccount(userAccount: GetUserAccount): UserAccount {
return userAccountRepository.findByUserIdOrThrow(userAccount.userId, userAccount.identifierType)
.toDto()
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
override fun getOrNull(username: String, identifierType: IdentifierType): User? { override fun getOrNull(username: String, identifierType: IdentifierType): User? {
return userAccountRepository.findByUsername(username, identifierType) return userAccountRepository.findByUsername(username, identifierType)

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105011900__create_table__devices id: 202105011900__create_table__devices
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: devices tableName: devices

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105052300__create_table__users id: 202105052300__create_table__users
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: users tableName: users

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105052310__create_table__user_accounts id: 202105052310__create_table__user_accounts
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: user_accounts tableName: user_accounts

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105052331__create_table__devices_users id: 202105052331__create_table__devices_users
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: devices_users tableName: devices_users

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105081411__create_table__scopes id: 202105081411__create_table__scopes
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: scopes tableName: scopes

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105081431__create_table__scope_groups id: 202105081431__create_table__scope_groups
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: scope_groups tableName: scope_groups

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105081531__create_table__users_scopes id: 202105081531__create_table__users_scopes
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: users_scopes tableName: users_scopes

View File

@ -8,12 +8,12 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.dao.DataAccessException import org.springframework.dao.DataAccessException
import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException
import ru.touchin.auth.core.device.models.DeviceEntity 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.DeviceRepository
import ru.touchin.auth.core.device.repository.findByIdOrThrow import ru.touchin.auth.core.device.repository.findByIdOrThrow
import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow
import ru.touchin.auth.core.user.models.UserEntity import ru.touchin.auth.core.user.models.UserEntity
import ru.touchin.auth.core.user.repositories.UserRepository import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*

View File

@ -14,8 +14,8 @@ import org.springframework.test.context.ActiveProfiles
import ru.touchin.auth.core.device.dto.Device import ru.touchin.auth.core.device.dto.Device
import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException
import ru.touchin.auth.core.device.models.DeviceEntity 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.DeviceRepository
import ru.touchin.common.devices.enums.DevicePlatform
import java.util.* import java.util.*
@ActiveProfiles("test") @ActiveProfiles("test")

View File

@ -48,7 +48,7 @@ internal class ScopeRepositoryTest {
val savedScope = scopeRepository.findByIdOrThrow(scope.name) val savedScope = scopeRepository.findByIdOrThrow(scope.name)
assertTrue( assertTrue(
ReflectionEquals(scope, "createdAt", "users").matches(savedScope) ReflectionEquals(scope, "createdAt").matches(savedScope)
) )
} }

View File

@ -9,12 +9,12 @@ import org.mockito.internal.matchers.apachecommons.ReflectionEquals
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.dao.DataAccessException import org.springframework.dao.DataAccessException
import ru.touchin.auth.core.device.models.DeviceEntity 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.DeviceRepository
import ru.touchin.auth.core.scope.models.ScopeEntity import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.scope.repositories.ScopeRepository import ru.touchin.auth.core.scope.repositories.ScopeRepository
import ru.touchin.auth.core.user.exceptions.UserNotFoundException import ru.touchin.auth.core.user.exceptions.UserNotFoundException
import ru.touchin.auth.core.user.models.UserEntity import ru.touchin.auth.core.user.models.UserEntity
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.* import java.util.*
@ -43,7 +43,7 @@ internal class UserRepositoryTest {
anonymous = false anonymous = false
confirmedAt = ZonedDateTime.now() confirmedAt = ZonedDateTime.now()
devices = emptySet() devices = emptySet()
scopes = mutableSetOf() scopes = emptySet()
} }
) )
@ -51,7 +51,7 @@ internal class UserRepositoryTest {
val savedUser = userRepository.findByIdOrThrow(user.id!!) val savedUser = userRepository.findByIdOrThrow(user.id!!)
assertTrue(ReflectionEquals(user, "createdAt", "confirmedAt").matches(savedUser)) assertTrue(ReflectionEquals(user, "createdAt").matches(savedUser))
} }
@ -87,7 +87,7 @@ internal class UserRepositoryTest {
val user = UserEntity() val user = UserEntity()
.apply { .apply {
scopes = mutableSetOf(scope) scopes = setOf(scope)
} }
userRepository.saveAndFlush(user) userRepository.saveAndFlush(user)
@ -117,7 +117,7 @@ internal class UserRepositoryTest {
fun shouldBeErrorIfScopeNew() { fun shouldBeErrorIfScopeNew() {
val user = UserEntity() val user = UserEntity()
.apply { .apply {
scopes = mutableSetOf(ScopeEntity().apply { name = "admin" }) scopes = setOf(ScopeEntity().apply { name = "admin" })
} }
assertThrows(DataAccessException::class.java) { assertThrows(DataAccessException::class.java) {

View File

@ -2,7 +2,6 @@ package ru.touchin.auth.core.user.services
import org.junit.jupiter.api.Assertions.assertDoesNotThrow import org.junit.jupiter.api.Assertions.assertDoesNotThrow
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
@ -11,11 +10,10 @@ import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import ru.touchin.auth.core.device.dto.Device 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.DeviceRepository
import ru.touchin.auth.core.device.repository.findByIdOrThrow import ru.touchin.auth.core.device.repository.findByIdOrThrow
import ru.touchin.auth.core.device.services.DeviceCoreService import ru.touchin.auth.core.device.services.DeviceCoreService
import ru.touchin.auth.core.scope.dto.Scope
import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.user.dto.User import ru.touchin.auth.core.user.dto.User
import ru.touchin.auth.core.user.dto.enums.IdentifierType import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.auth.core.user.exceptions.UserAlreadyRegisteredException import ru.touchin.auth.core.user.exceptions.UserAlreadyRegisteredException
@ -23,14 +21,11 @@ import ru.touchin.auth.core.user.exceptions.WrongPasswordException
import ru.touchin.auth.core.user.repositories.UserAccountRepository import ru.touchin.auth.core.user.repositories.UserAccountRepository
import ru.touchin.auth.core.user.repositories.UserRepository import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.auth.core.user.repositories.findByIdOrThrow import ru.touchin.auth.core.user.repositories.findByIdOrThrow
import ru.touchin.auth.core.user.services.dto.AddUserScopes
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser 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.NewUser
import ru.touchin.auth.core.user.services.dto.UserLogin import ru.touchin.auth.core.user.services.dto.UserLogin
import ru.touchin.auth.core.user.services.dto.UserLogout
import ru.touchin.auth.core.user.services.dto.UserUpdatePassword
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
import java.lang.IllegalArgumentException
import java.util.* import java.util.*
import javax.persistence.EntityManager import javax.persistence.EntityManager
@ -287,128 +282,4 @@ internal class UserCoreServiceImplSlowTest {
assertEquals(loginUserE2.id, actualDeviceE2.users.first().id!!) assertEquals(loginUserE2.id, actualDeviceE2.users.first().id!!)
} }
@Test
@DisplayName("При логауте пользователь отлинковывается от устройства")
fun usersShouldBeUnbindedAfterLogout() {
val device = createDevice()
createNewUser(deviceId = device.id, password = "qwerty", username = "employee1")
val regUserE2 = createNewUser(deviceId = device.id, password = "qwerty", username = "employee2")
val actualDeviceE2 = deviceRepository.findByIdOrThrow(deviceId = device.id)
assertEquals(1, actualDeviceE2.users.size)
assertEquals(regUserE2.id, actualDeviceE2.users.first().id!!)
userCoreService.logout(
UserLogout(
deviceId = device.id,
userId = regUserE2.id
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualDevice = deviceRepository.findByIdOrThrow(deviceId = device.id)
assertTrue(actualDevice.users.isEmpty())
}
@Test
@DisplayName("Пользователь может залогиниться на нескольких устройствах")
fun usersShouldOkTwoDevicesLogin() {
val deviceD1 = createDevice()
val deviceD2 = createDevice()
val regUser = createNewUser(deviceId = deviceD1.id, password = "qwerty", username = "employee")
userCoreService.login(
UserLogin(
deviceId = deviceD2.id,
username = "employee",
password = "qwerty",
identifierType = IdentifierType.Email,
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualUser = userRepository.findByIdOrThrow(regUser.id)
assertEquals(2, actualUser.devices.size)
userCoreService.logout(
UserLogout(
deviceId = deviceD1.id,
userId = regUser.id
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualUser2 = userRepository.findByIdOrThrow(regUser.id)
assertEquals(1, actualUser2.devices.size)
assertEquals(deviceD2.id, actualUser2.devices.first().id!!)
}
@Test
@DisplayName("Пользователь может сменить пароль")
fun userCanChangePassword() {
val device = createDevice()
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee1")
val userAccount = userAccountRepository.findByUserId(regUser.id, IdentifierType.Email)
assertNotNull(userAccount)
userCoreService.updatePassword(
UserUpdatePassword(
userAccountId = userAccount?.id!!,
oldPassword = "qwerty",
newPassword = "QWERTY1234"
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualUserAccount = userAccountRepository.findByUsername("employee1", IdentifierType.Email)
assertNotNull(actualUserAccount)
assertFalse(passwordEncoder.matches(userAccount.password!!, actualUserAccount?.password!!))
assertTrue(passwordEncoder.matches("QWERTY1234", actualUserAccount.password!!))
}
@Test
@DisplayName("Пользователю можно добавить скоупы")
fun userCanAddScope() {
val device = createDevice()
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee1")
val newScopes = listOf("admin", "moderator", "robot")
userCoreService.addScopes(
AddUserScopes(
userId = regUser.id,
scopes = newScopes
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualUser = userRepository.findByIdOrThrow(regUser.id)
val expectedScopes: List<String> = regUser.scopes.map(Scope::name) + newScopes
assertTrue(actualUser.scopes.map(ScopeEntity::name).containsAll(expectedScopes))
assertEquals(expectedScopes.size, actualUser.scopes.size)
}
} }

View File

@ -18,7 +18,10 @@ import org.springframework.test.context.ActiveProfiles
import ru.touchin.auth.core.device.converters.DeviceConverter.toDto import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
import ru.touchin.auth.core.device.exceptions.DeviceAlreadyLinkedException import ru.touchin.auth.core.device.exceptions.DeviceAlreadyLinkedException
import ru.touchin.auth.core.device.models.DeviceEntity 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.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.dto.Scope
import ru.touchin.auth.core.scope.models.ScopeEntity import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.scope.repositories.ScopeRepository import ru.touchin.auth.core.scope.repositories.ScopeRepository
@ -29,7 +32,6 @@ import ru.touchin.auth.core.user.repositories.UserAccountRepository
import ru.touchin.auth.core.user.repositories.UserRepository 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.NewAnonymousUser
import ru.touchin.auth.core.user.services.dto.NewUser import ru.touchin.auth.core.user.services.dto.NewUser
import ru.touchin.common.devices.enums.DevicePlatform
import java.util.* import java.util.*
@ActiveProfiles("test") @ActiveProfiles("test")

View File

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

View File

@ -5,9 +5,7 @@ plugins {
} }
dependencies { dependencies {
implementation(project(":security-authorization-server-core")) implementation(project(":auth-core"))
implementation(project(":security-resource-server-custom-jwt-configuration"))
implementation(project(":security-jwt-common"))
implementation("com.auth0:java-jwt") implementation("com.auth0:java-jwt")
implementation("org.springframework.security:spring-security-oauth2-jose") implementation("org.springframework.security:spring-security-oauth2-jose")

View File

@ -1,11 +1,11 @@
@file:Suppress("unused")
package ru.touchin.auth.core.configurations package ru.touchin.auth.core.configurations
import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import import org.springframework.context.annotation.Import
@Configuration
@Import(AuthCoreConfiguration::class) @Import(AuthCoreConfiguration::class)
@ComponentScan("ru.touchin.auth.core.tokens") @ComponentScan("ru.touchin.auth.core.tokens")
@ConfigurationPropertiesScan("ru.touchin.auth.core.tokens") @ConfigurationPropertiesScan("ru.touchin.auth.core.tokens")

View File

@ -1,10 +1,10 @@
@file:Suppress("unused")
package ru.touchin.auth.core.configurations package ru.touchin.auth.core.configurations
import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import import org.springframework.context.annotation.Import
@Configuration
@Import(AuthCoreDatabaseConfiguration::class) @Import(AuthCoreDatabaseConfiguration::class)
@EntityScan("ru.touchin.auth.core.tokens") @EntityScan("ru.touchin.auth.core.tokens")
class AuthTokenDatabaseConfiguration class AuthTokenDatabaseConfiguration

View File

@ -1,33 +1,35 @@
package ru.touchin.auth.core.tokens.access.config package ru.touchin.auth.core.tokens.access.config
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import ru.touchin.auth.core.tokens.access.properties.AccessTokenProperties import ru.touchin.auth.core.tokens.access.properties.AccessTokenProperties
import ru.touchin.auth.security.jwt.configurations.JwtConfiguration import ru.touchin.common.string.StringUtils.emptyString
import ru.touchin.auth.security.jwt.utils.JwtUtils.getKeySpec
import java.security.KeyFactory import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
@Configuration @Configuration
@Import(JwtConfiguration::class)
class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProperties) { class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProperties) {
@Bean @Bean
fun accessTokenSigningAlgorithm( fun accessTokenSigningAlgorithm(): Algorithm {
@Qualifier("accessTokenPublicKey")
accessTokenPublicKey: RSAPublicKey
): Algorithm {
return Algorithm.RSA256( return Algorithm.RSA256(
accessTokenPublicKey, accessTokenPublicKey(),
accessTokenPrivateKey() accessTokenPrivateKey()
) )
} }
@Bean("accessTokenPublicKey")
fun accessTokenPublicKey(): RSAPublicKey {
val keySpecX509 = getKeySpec(accessTokenProperties.keyPair.public, ::X509EncodedKeySpec)
return keyFactory.generatePublic(keySpecX509) as RSAPublicKey
}
@Bean("accessTokenPrivateKey") @Bean("accessTokenPrivateKey")
fun accessTokenPrivateKey(): RSAPrivateKey { fun accessTokenPrivateKey(): RSAPrivateKey {
val keySpecPKCS8 = getKeySpec(accessTokenProperties.keyPair.private, ::PKCS8EncodedKeySpec) val keySpecPKCS8 = getKeySpec(accessTokenProperties.keyPair.private, ::PKCS8EncodedKeySpec)
@ -35,6 +37,22 @@ class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProper
return keyFactory.generatePrivate(keySpecPKCS8) as RSAPrivateKey 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 { companion object {
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA") val keyFactory: KeyFactory = KeyFactory.getInstance("RSA")
} }

View File

@ -1,5 +1,3 @@
@file:Suppress("unused")
package ru.touchin.auth.core.tokens.access.exceptions package ru.touchin.auth.core.tokens.access.exceptions
import ru.touchin.common.exceptions.CommonException import ru.touchin.common.exceptions.CommonException

View File

@ -3,7 +3,7 @@ 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.AccessToken
import ru.touchin.auth.core.tokens.access.dto.AccessTokenRequest import ru.touchin.auth.core.tokens.access.dto.AccessTokenRequest
interface AccessTokenCoreService { interface AccessTokenService {
fun create(accessTokenRequest: AccessTokenRequest): AccessToken fun create(accessTokenRequest: AccessTokenRequest): AccessToken

View File

@ -13,10 +13,10 @@ import java.time.ZoneId
import java.util.* import java.util.*
@Service @Service
class JwtAccessTokenCoreServiceImpl( class JwtAccessTokenServiceImpl(
private val accessTokenProperties: AccessTokenProperties, private val accessTokenProperties: AccessTokenProperties,
private val accessTokenSigningAlgorithm: Algorithm private val accessTokenSigningAlgorithm: Algorithm
) : AccessTokenCoreService { ) : AccessTokenService {
private fun sign(builder: JWTCreator.Builder) = builder.sign(accessTokenSigningAlgorithm) private fun sign(builder: JWTCreator.Builder) = builder.sign(accessTokenSigningAlgorithm)

View File

@ -6,6 +6,5 @@ import java.time.ZonedDateTime
data class RefreshToken( data class RefreshToken(
val value: String, val value: String,
val expiresAt: ZonedDateTime, val expiresAt: ZonedDateTime,
val usedAt: ZonedDateTime?,
val user: User, val user: User,
) )

View File

@ -1,8 +1,5 @@
@file:Suppress("unused")
package ru.touchin.auth.core.tokens.refresh.models package ru.touchin.auth.core.tokens.refresh.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.auth.core.device.models.DeviceEntity import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.scope.models.ScopeEntity import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.tokens.refresh.exceptions.RefreshTokenExpiredException import ru.touchin.auth.core.tokens.refresh.exceptions.RefreshTokenExpiredException
@ -18,15 +15,13 @@ import javax.persistence.ManyToOne
import javax.persistence.Table import javax.persistence.Table
@Entity @Entity
@Table(name = "refresh_tokens", schema = SCHEMA) @Table(name = "refresh_tokens")
class RefreshTokenEntity : AuditableUuidIdEntity() { class RefreshTokenEntity : AuditableUuidIdEntity() {
lateinit var value: String lateinit var value: String
lateinit var expiresAt: ZonedDateTime lateinit var expiresAt: ZonedDateTime
var usedAt: ZonedDateTime? = null
@ManyToOne @ManyToOne
@JoinColumn(name = "user_id") @JoinColumn(name = "user_id")
lateinit var user: UserEntity lateinit var user: UserEntity
@ -38,14 +33,13 @@ class RefreshTokenEntity : AuditableUuidIdEntity() {
@ManyToMany @ManyToMany
@JoinTable( @JoinTable(
name = "refresh_token_scopes", name = "refresh_token_scopes",
schema = SCHEMA,
joinColumns = [JoinColumn(name = "refresh_token_id")], joinColumns = [JoinColumn(name = "refresh_token_id")],
inverseJoinColumns = [JoinColumn(name = "scope_name")] inverseJoinColumns = [JoinColumn(name = "scope_name")]
) )
lateinit var scopes: Set<ScopeEntity> lateinit var scopes: Set<ScopeEntity>
fun validate(): RefreshTokenEntity = this.apply { fun validate(): RefreshTokenEntity = this.apply {
if (expiresAt.isExpired() || usedAt != null) { if (expiresAt.isExpired()) {
throw RefreshTokenExpiredException(value) throw RefreshTokenExpiredException(value)
} }
} }

View File

@ -3,10 +3,9 @@ package ru.touchin.auth.core.tokens.refresh.services
import ru.touchin.auth.core.tokens.refresh.dto.RefreshToken import ru.touchin.auth.core.tokens.refresh.dto.RefreshToken
import ru.touchin.auth.core.tokens.refresh.services.dto.NewRefreshToken import ru.touchin.auth.core.tokens.refresh.services.dto.NewRefreshToken
interface RefreshTokenCoreService { interface RefreshTokenService {
fun get(value: String): RefreshToken fun get(value: String): RefreshToken
fun create(token: NewRefreshToken): RefreshToken fun create(token: NewRefreshToken): RefreshToken
fun refresh(value: String): RefreshToken
} }

View File

@ -7,6 +7,8 @@ import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
import ru.touchin.auth.core.device.repository.DeviceRepository import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.scope.dto.Scope import ru.touchin.auth.core.scope.dto.Scope
import ru.touchin.auth.core.scope.repositories.ScopeRepository 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.dto.RefreshToken
import ru.touchin.auth.core.tokens.refresh.models.RefreshTokenEntity 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.properties.RefreshTokenProperties
@ -14,29 +16,22 @@ 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.repositories.findByValueOrThrow
import ru.touchin.auth.core.tokens.refresh.services.dto.NewRefreshToken import ru.touchin.auth.core.tokens.refresh.services.dto.NewRefreshToken
import ru.touchin.auth.core.user.converters.UserConverter.toDto import ru.touchin.auth.core.user.converters.UserConverter.toDto
import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.auth.core.user.repositories.findByIdOrThrow
import ru.touchin.common.byte.ByteUtils.toHex
import ru.touchin.common.random.SecureRandomStringGenerator import ru.touchin.common.random.SecureRandomStringGenerator
import ru.touchin.common.security.hash.HashUtils
import ru.touchin.common.security.hash.HashUtils.calculateHash
import java.time.ZonedDateTime import java.time.ZonedDateTime
@Service @Service
class RefreshTokenCoreServiceImpl( class RefreshTokenServiceImpl(
private val refreshTokenProperties: RefreshTokenProperties, private val refreshTokenProperties: RefreshTokenProperties,
private val refreshTokenRepository: RefreshTokenRepository, private val refreshTokenRepository: RefreshTokenRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val deviceRepository: DeviceRepository, private val deviceRepository: DeviceRepository,
private val scopeRepository: ScopeRepository, private val scopeRepository: ScopeRepository,
) : RefreshTokenCoreService { ) : RefreshTokenService {
@Transactional(readOnly = true) @Transactional(readOnly = true)
override fun get(value: String): RefreshToken { override fun get(value: String): RefreshToken {
val valueHash = getTokenHash(value) return refreshTokenRepository.findByValueOrThrow(value)
.toDto()
return refreshTokenRepository.findByValueOrThrow(valueHash)
.toDto(value)
} }
@Transactional @Transactional
@ -44,44 +39,17 @@ class RefreshTokenCoreServiceImpl(
val user = userRepository.findByIdOrThrow(token.userId) val user = userRepository.findByIdOrThrow(token.userId)
val device = token.deviceId?.let(deviceRepository::findByIdOrNull) val device = token.deviceId?.let(deviceRepository::findByIdOrNull)
val scopes = scopeRepository.findAllById(token.scopes.map(Scope::name)) val scopes = scopeRepository.findAllById(token.scopes.map(Scope::name))
val value = generateTokenValue()
val model = RefreshTokenEntity().apply { val model = RefreshTokenEntity().apply {
expiresAt = getExpirationDate() expiresAt = getExpirationDate()
this.value = getTokenHash(value) value = generateTokenValue()
this.user = user this.user = user
this.device = device this.device = device
this.scopes = scopes.toSet() this.scopes = scopes.toSet()
} }
return refreshTokenRepository.save(model) return refreshTokenRepository.save(model)
.toDto(value) .toDto()
}
@Transactional
override fun refresh(value: String): RefreshToken {
val valueHash = getTokenHash(value)
val oldToken = refreshTokenRepository.findByValueOrThrow(valueHash)
.validate()
.apply {
usedAt = ZonedDateTime.now()
}
refreshTokenRepository.save(oldToken)
val newValue = generateTokenValue()
val model = RefreshTokenEntity().apply {
this.value = getTokenHash(newValue)
expiresAt = getExpirationDate()
user = oldToken.user
device = oldToken.device
scopes = oldToken.scopes.toSet()
}
return refreshTokenRepository.save(model)
.toDto(newValue)
} }
private fun getExpirationDate(): ZonedDateTime { private fun getExpirationDate(): ZonedDateTime {
@ -94,20 +62,13 @@ class RefreshTokenCoreServiceImpl(
} }
} }
private fun getTokenHash(value: String): String {
return value.calculateHash(HashUtils.HashAlgorithm.SHA256)
.toHex()
}
companion object { companion object {
fun RefreshTokenEntity.toDto(): RefreshToken {
fun RefreshTokenEntity.toDto(rawValue: String): RefreshToken {
val device = device?.toDto() val device = device?.toDto()
return RefreshToken( return RefreshToken(
value = rawValue, value = value,
expiresAt = expiresAt, expiresAt = expiresAt,
usedAt = usedAt,
user = user.toDto(device) user = user.toDto(device)
) )
} }

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105201851__create_table__refresh_tokens id: 202105201851__create_table__refresh_tokens
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: refresh_tokens tableName: refresh_tokens

View File

@ -3,7 +3,7 @@ databaseChangeLog:
id: 202105201901__create_table__refresh_token_scopes id: 202105201901__create_table__refresh_token_scopes
author: touchin author: touchin
preConditions: preConditions:
- onFail: MARK_RAN onFail: MARK_RAN
not: not:
tableExists: tableExists:
tableName: refresh_token_scopes tableName: refresh_token_scopes

View File

@ -1,7 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.jetbrains.kotlin.cli.common.toBooleanLenient
plugins { plugins {
kotlin("jvm") kotlin("jvm")
@ -17,9 +15,6 @@ plugins {
// A Gradle plugin that provides Maven-like dependency management and exclusions // A Gradle plugin that provides Maven-like dependency management and exclusions
// https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/ // https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/
id("io.spring.dependency-management") id("io.spring.dependency-management")
id("io.gitlab.arturbosch.detekt")
id("org.cqfn.diktat.diktat-gradle-plugin")
} }
allprojects { allprojects {
@ -35,30 +30,6 @@ allprojects {
apply(plugin = "idea") apply(plugin = "idea")
} }
diktat {
inputs {
include(
"**/src/**/*.kt",
"*.kts",
"**/*.kts",
"**/src/**/*.kts",
)
exclude(
"**/build/**",
"build/**",
)
}
val tasksFileReportEnabled = project.properties["TASKS_FILE_REPORT_ENABLED"]
?.let { it.toString().toBooleanLenient() }
?: true
reporter = if (tasksFileReportEnabled) "html" else "plain"
output = if (tasksFileReportEnabled) "${project.buildDir}/reports/diktat-report.html" else String()
diktatConfigFile = file("$rootDir/diktat-analysis.yml")
ignoreFailures = true
debug = false
}
subprojects { subprojects {
println("Enabling Kotlin JVM plugin in project ${project.name}...") println("Enabling Kotlin JVM plugin in project ${project.name}...")
apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.jvm")
@ -69,26 +40,6 @@ subprojects {
println("Enabling Spring Boot Dependency Management in project ${project.name}...") println("Enabling Spring Boot Dependency Management in project ${project.name}...")
apply(plugin = "io.spring.dependency-management") apply(plugin = "io.spring.dependency-management")
println("Enabling Detekt support in project ${project.name}...")
apply(plugin = "io.gitlab.arturbosch.detekt")
detekt {
config = files("$rootDir/detekt-config.yml")
source = files(
DetektExtension.Companion.DEFAULT_SRC_DIR_JAVA,
DetektExtension.Companion.DEFAULT_SRC_DIR_KOTLIN,
)
reports {
txt.enabled = false
xml.enabled = false
html {
enabled = true
destination = file("${project.buildDir}/reports/kotlin-detekt-${project.name}.html")
}
}
}
configure<DependencyManagementExtension> { configure<DependencyManagementExtension> {
imports { imports {
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
@ -99,17 +50,13 @@ subprojects {
dependency("ch.qos.logback.contrib:logback-json-classic:0.1.5") dependency("ch.qos.logback.contrib:logback-json-classic:0.1.5")
dependency("ch.qos.logback.contrib:logback-jackson:0.1.5") dependency("ch.qos.logback.contrib:logback-jackson:0.1.5")
dependency("org.testcontainers:testcontainers:1.18.3") dependency("org.testcontainers:testcontainers:1.15.1")
dependency("org.testcontainers:postgresql:1.18.3") dependency("org.testcontainers:postgresql:1.15.1")
dependency("org.testcontainers:junit-jupiter:1.18.3") dependency("org.testcontainers:junit-jupiter:1.15.1")
dependency("org.junit.jupiter:junit-jupiter-api:5.4.2") dependency("org.junit.jupiter:junit-jupiter-api:5.4.2")
dependency("org.junit.jupiter:junit-jupiter-params:5.4.2") dependency("org.junit.jupiter:junit-jupiter-params:5.4.2")
dependency("org.junit.jupiter:junit-jupiter-engine:5.4.2") dependency("org.junit.jupiter:junit-jupiter-engine:5.4.2")
dependency("com.tngtech.archunit:archunit:1.0.1")
dependency("org.liquibase:liquibase-core:4.4.0")
dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
dependency("org.mockito:mockito-inline:3.11.0") dependency("org.mockito:mockito-inline:3.11.0")
@ -121,12 +68,6 @@ subprojects {
dependency("org.locationtech.spatial4j:spatial4j:0.8") dependency("org.locationtech.spatial4j:spatial4j:0.8")
dependency("com.auth0:java-jwt:3.10.3") dependency("com.auth0:java-jwt:3.10.3")
dependency("software.amazon.awssdk:s3:2.10.11")
dependency("com.google.firebase:firebase-admin:9.0.0")
dependency("com.github.ua-parser:uap-java:1.5.3")
} }
} }

View File

@ -1,15 +0,0 @@
plugins {
id("kotlin")
id("kotlin-spring")
}
dependencies {
implementation(project(":common"))
implementation(project(":common-spring-web"))
implementation(project(":logger-spring"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-aop")
}

View File

@ -1,9 +0,0 @@
package ru.touchin.captcha.annotations
import ru.touchin.captcha.dto.enums.CaptchaScore
@Target(AnnotationTarget.FUNCTION)
annotation class Captcha(
val action: String,
val minScore: CaptchaScore = CaptchaScore.AVERAGE
)

View File

@ -1,39 +0,0 @@
package ru.touchin.captcha.aspects
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import ru.touchin.captcha.annotations.Captcha
import ru.touchin.captcha.exceptions.CaptchaResponseMissingException
import ru.touchin.captcha.services.CaptchaService
@Aspect
@Component
@ConditionalOnProperty(prefix = "captcha", name = ["enabled"], havingValue = "true", matchIfMissing = true)
class CaptchaSiteVerifyAspect(private val captchaService: CaptchaService) {
@Throws(Throwable::class)
@Before("@annotation(captcha)")
fun captchaSiteVerify(captcha: Captcha) {
val currentRequestAttributes = RequestContextHolder.currentRequestAttributes()
as? ServletRequestAttributes
?: throw IllegalStateException("unable to get current request attributes")
val captchaResponse = currentRequestAttributes.request.getHeader(CAPTCHA_RESPONSE_HEADER_NAME)
?: throw CaptchaResponseMissingException()
captchaService.verify(captchaResponse)
.validateAction(captcha.action)
.validateScore(captcha.minScore.value)
}
companion object {
private const val CAPTCHA_RESPONSE_HEADER_NAME = "X-Captcha-Response"
}
}

View File

@ -1,8 +0,0 @@
package ru.touchin.captcha.configurations
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.ComponentScan
@ComponentScan("ru.touchin.captcha")
@ConfigurationPropertiesScan("ru.touchin.captcha")
class CaptchaConfiguration

View File

@ -1,24 +0,0 @@
package ru.touchin.captcha.dto
import ru.touchin.captcha.exceptions.CaptchaActionMismatchException
import ru.touchin.captcha.exceptions.CaptchaScoreBelowMinimumException
data class CaptchaVerificationResult(val score: Double, val action: String) {
fun validateScore(minScore: Double): CaptchaVerificationResult {
if (score < minScore) {
throw CaptchaScoreBelowMinimumException(score)
}
return this
}
fun validateAction(actionToValidate: String): CaptchaVerificationResult {
if (action != actionToValidate) {
throw CaptchaActionMismatchException(actionToValidate)
}
return this
}
}

View File

@ -1,9 +0,0 @@
package ru.touchin.captcha.dto.enums
enum class CaptchaScore(val value: Double) {
WEAK(0.2),
AVERAGE(0.5),
STRONG(0.8)
}

View File

@ -1,12 +0,0 @@
package ru.touchin.captcha.dto.response
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.ZonedDateTime
data class CaptchaSiteVerifyResponse(
val score: Double,
val action: String,
val success: Boolean,
@JsonProperty("challenge_ts")
val challengeTs: ZonedDateTime
)

View File

@ -1,7 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaActionMismatchException(
action: String
) : CommonException("invalid captcha action $action")

View File

@ -1,5 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaResponseMissingException : CommonException("missing captcha response header")

View File

@ -1,7 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaScoreBelowMinimumException(
score: Double,
) : CommonException("captcha score below minimum $score")

View File

@ -1,7 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaSiteVerifyFailureException(
description: String?
) : CommonException(description)

View File

@ -1,7 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaUnknownActionException(
action: String
) : CommonException("unknown captcha action $action")

View File

@ -1,13 +0,0 @@
package ru.touchin.captcha.properties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import java.net.URI
@ConstructorBinding
@ConfigurationProperties(prefix = "captcha")
data class CaptchaProperties(
val uri: URI,
val secret: String,
val actions: List<String>
)

View File

@ -1,9 +0,0 @@
package ru.touchin.captcha.services
import ru.touchin.captcha.dto.CaptchaVerificationResult
interface CaptchaService {
fun verify(response: String): CaptchaVerificationResult
}

View File

@ -1,33 +0,0 @@
package ru.touchin.captcha.services.impl
import org.springframework.stereotype.Service
import ru.touchin.captcha.dto.CaptchaVerificationResult
import ru.touchin.captcha.exceptions.CaptchaUnknownActionException
import ru.touchin.captcha.properties.CaptchaProperties
import ru.touchin.captcha.services.CaptchaService
import ru.touchin.captcha.webclients.CaptchaWebClient
import ru.touchin.logger.spring.annotations.AutoLogging
import ru.touchin.logger.spring.annotations.LogValue
@Service
class CaptchaServiceImpl(
private val captchaWebClient: CaptchaWebClient,
private val captchaProperties: CaptchaProperties,
) : CaptchaService {
@LogValue
@AutoLogging(tags = ["CAPTCHA", "CAPTCHA_VERIFICATION"])
override fun verify(response: String): CaptchaVerificationResult {
val siteVerifyResponse = captchaWebClient.siteVerify(response)
if (siteVerifyResponse.action !in captchaProperties.actions) {
throw CaptchaUnknownActionException(siteVerifyResponse.action)
}
return CaptchaVerificationResult(
score = siteVerifyResponse.score,
action = siteVerifyResponse.action,
)
}
}

View File

@ -1,55 +0,0 @@
package ru.touchin.captcha.webclients
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import ru.touchin.captcha.properties.CaptchaProperties
import ru.touchin.captcha.dto.response.CaptchaSiteVerifyResponse
import ru.touchin.captcha.exceptions.CaptchaSiteVerifyFailureException
import ru.touchin.common.spring.web.webclient.BaseLogWebClient
import ru.touchin.common.spring.web.webclient.dto.RequestLogData
import ru.touchin.common.spring.web.webclient.logger.WebClientLogger
private const val SITE_VERIFY_PATH = "/recaptcha/api/siteverify"
@Component
class CaptchaWebClient(
webClientLogger: WebClientLogger,
webClientBuilder: WebClient.Builder,
private val captchaProperties: CaptchaProperties,
) : BaseLogWebClient(webClientLogger, webClientBuilder) {
override fun getWebClient(): WebClient {
return getWebClientBuilder(captchaProperties.uri.toString()).build()
}
fun siteVerify(response: String): CaptchaSiteVerifyResponse {
return getWebClient().post()
.uri {
it.path(SITE_VERIFY_PATH)
it.queryParam("response", response)
it.queryParam("secret", captchaProperties.secret)
it.build()
}
.accept(MediaType.APPLICATION_JSON)
.exchange(
clazz = CaptchaSiteVerifyResponse::class.java,
requestLogData = RequestLogData(
uri = SITE_VERIFY_PATH,
logTags = listOf("CAPTCHA_SITEVERIFY"),
method = HttpMethod.POST,
)
)
.block()!!
.also(this::validateSuccess)
}
private fun validateSuccess(captchaSiteVerifyResponse: CaptchaSiteVerifyResponse) {
if (!captchaSiteVerifyResponse.success) {
throw CaptchaSiteVerifyFailureException("Captcha siteverify request did not succeed")
}
}
}

View File

@ -1,8 +0,0 @@
plugins {
id("kotlin")
}
dependencies {
implementation(project(":common"))
implementation("com.tngtech.archunit:archunit")
}

View File

@ -1,53 +0,0 @@
package ru.touchin.codestyle.archunit.rules
import com.tngtech.archunit.lang.ArchRule
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods
import ru.touchin.common.exceptions.CommonException
/**
* Set of rules that defines constraint for classes naming.
*/
@Suppress("unused")
object ClassNamingArchRules {
val CLASSES_WHICH_HAVE_SCHEDULED_METHODS_MUST_HAVE_SUFFIX: ArchRule = methods()
.that()
.areAnnotatedWith("org.springframework.scheduling.annotation.Scheduled")
.should().beDeclaredInClassesThat().haveSimpleNameEndingWith("Job")
.allowEmptyShould(true)
.because("Classes that use 'Scheduled' annotation must have name with 'Job' suffix")
val ANNOTATED_SERVICE_CLASSES_MUST_HAVE_SUFFIX: ArchRule = methods()
.that().areAnnotatedWith("org.springframework.stereotype.Service")
.should().beDeclaredInClassesThat().haveSimpleNameContaining("Service")
.allowEmptyShould(true)
.because("Classes that use 'Service' annotation are assignable to class with `Service` suffix")
val ANNOTATED_ENTITY_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAnnotatedWith("javax.persistence.Entity")
.should().haveSimpleNameEndingWith("Entity")
.allowEmptyShould(true)
.because("Hibernate 'Entity' classes must have name with 'Entity' suffix")
val EXCEPTION_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAssignableTo(Exception::class.java)
.or().areAssignableTo(CommonException::class.java)
.should().haveSimpleNameEndingWith("Exception")
.allowEmptyShould(true)
.because("'Exception' classes must have name with 'Exception' suffix")
val JPAREPOSITORY_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAssignableTo("org.springframework.data.jpa.repository.JpaRepository")
.should().haveSimpleNameEndingWith("Repository")
.allowEmptyShould(true)
.because("Classes that use 'JpaRepository' must have name with 'Repository' suffix")
val WEBCLIENT_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().haveFullyQualifiedName("org.springframework.web.reactive.valction.client.WebClient")
.should().onlyBeAccessed().byClassesThat().haveSimpleNameEndingWith("WebClient")
.allowEmptyShould(true)
.because("Classes that use Spring 'WebClient' must have name with 'WebClient' suffix")
}

View File

@ -1,56 +0,0 @@
package ru.touchin.codestyle.archunit.rules
import com.tngtech.archunit.lang.ArchRule
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
/**
* Set of rules that defines constraint for classes placement.
*/
@Suppress("unused")
object ClassPackagingArchRules {
val IMPL_CLASSES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Impl")
.should().resideInAPackage("..impl..")
.allowEmptyShould(true)
.because("Implementations of interfaces must reside in package 'impl'")
val ENTITIES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Entity")
.should().resideInAPackage("..models..")
.allowEmptyShould(true)
.because("'Entity' must reside in package 'models'")
val REPOSITORIES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Repository")
.should().resideInAPackage("..repositories..")
.allowEmptyShould(true)
.because("'Repository' must reside in package 'repositories'")
val CORE_SERVICES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("CoreService")
.should().resideInAPackage("..services..")
.allowEmptyShould(true)
.because("'CoreService' must reside in package 'services'")
val CONVERTERS_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Converter")
.should().resideInAPackage("..converters..")
.allowEmptyShould(true)
.because("'Converter' must reside in package 'converters'")
val EXCEPTIONS_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Exception")
.should().resideInAPackage("..exceptions..")
.allowEmptyShould(true)
.because("'Exception' must reside in package 'exceptions'")
val ENUM_CLASSES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().areEnums()
.should().resideInAPackage("..enums..")
.allowEmptyShould(true)
.because("'Enum' must reside in package 'enums'")
}

Some files were not shown because too many files have changed in this diff Show More