Compare commits
No commits in common. "master" and "feature/auth" have entirely different histories.
master
...
feature/au
328
README.md
328
README.md
|
|
@ -19,19 +19,6 @@
|
|||
implementation("ru.touchin:common")
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
Основные компоненты логирования:
|
||||
|
|
@ -202,17 +89,11 @@ Interceptor для логирования запросов/ответов.
|
|||
|
||||
## exception-handler-spring-web
|
||||
|
||||
Перехватывает ошибки сервера, определяет код ошибки и возвращает их в правильный `response`.
|
||||
Подключается с помощью аннотации `@EnableSpringExceptionHandler`
|
||||
Перехватывает ошибки сервера, определяет код ошибки и возвращает их в правильный `response`
|
||||
|
||||
## exception-handler-logger-spring-web
|
||||
|
||||
Добавляет логирование в обработку ошибок.
|
||||
Подключается с помощью аннотации `@EnableSpringExceptionHandlerLogger` до подключения основного модуля.
|
||||
|
||||
## validation-spring
|
||||
|
||||
Добавляет аннотации для валидации запросов.
|
||||
Добавляет логирование в обработку ошибок
|
||||
|
||||
## version-spring-web
|
||||
|
||||
|
|
@ -242,11 +123,11 @@ Interceptor для логирования запросов/ответов.
|
|||
|
||||
Модуль для хранения настроек
|
||||
|
||||
## security-authorization-server-core
|
||||
## auth-core
|
||||
|
||||
Модуль авторизации
|
||||
|
||||
## security-authorization-server-jwt-core
|
||||
## auth-jwt-core
|
||||
|
||||
Добавляет поддержку jwt-токенов (создание/хранение). Для работы этого модуля требуется прописать в пропертях:
|
||||
|
||||
|
|
@ -267,204 +148,3 @@ token.refresh:
|
|||
prefix: RT-
|
||||
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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ dependencies {
|
|||
runtimeOnly("org.postgresql:postgresql")
|
||||
|
||||
api(project(":common"))
|
||||
api(project(":common-device"))
|
||||
|
||||
api(project(":common-spring-jpa"))
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
|||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
|
||||
@ComponentScan("ru.touchin.auth.core")
|
||||
@ConfigurationPropertiesScan("ru.touchin.auth.core")
|
||||
@ConfigurationPropertiesScan
|
||||
class AuthCoreConfiguration {
|
||||
|
||||
@Bean
|
||||
|
|
@ -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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
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.*
|
||||
|
||||
data class Device(
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.auth.core.device.dto.enums
|
||||
|
||||
enum class DevicePlatform {
|
||||
Android, Huawei, Apple
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
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.common.devices.enums.DevicePlatform
|
||||
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.JoinColumn
|
||||
|
|
@ -11,7 +10,7 @@ import javax.persistence.ManyToMany
|
|||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "devices", schema = SCHEMA)
|
||||
@Table(name = "devices")
|
||||
class DeviceEntity: AuditableUuidIdEntity() {
|
||||
|
||||
lateinit var platform: DevicePlatform
|
||||
|
|
@ -19,26 +18,11 @@ class DeviceEntity: AuditableUuidIdEntity() {
|
|||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "devices_users",
|
||||
schema = SCHEMA,
|
||||
joinColumns = [JoinColumn(name = "device_id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "user_id")]
|
||||
)
|
||||
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 {
|
||||
fun create(platform: DevicePlatform): DeviceEntity {
|
||||
return DeviceEntity().apply {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package ru.touchin.auth.core.device.services
|
||||
|
||||
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
|
||||
|
||||
interface DeviceCoreService {
|
||||
|
|
@ -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.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 ru.touchin.common.devices.enums.DevicePlatform
|
||||
import java.util.*
|
||||
|
||||
@Service
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
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 javax.persistence.Entity
|
||||
import javax.persistence.JoinColumn
|
||||
|
|
@ -10,7 +7,7 @@ import javax.persistence.ManyToOne
|
|||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "scope_groups", schema = SCHEMA)
|
||||
@Table(name = "scope_groups")
|
||||
class ScopeGroupEntity : BaseUuidIdEntity() {
|
||||
|
||||
lateinit var groupName: String
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
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.common.spring.jpa.models.AuditableUuidIdEntity
|
||||
import java.time.ZonedDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
|
|
@ -11,7 +11,7 @@ import javax.persistence.ManyToOne
|
|||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_accounts", schema = SCHEMA)
|
||||
@Table(name = "user_accounts")
|
||||
class UserAccountEntity: AuditableUuidIdEntity() {
|
||||
|
||||
lateinit var username: String
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
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.scope.models.ScopeEntity
|
||||
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
|
||||
|
|
@ -12,8 +11,8 @@ import javax.persistence.ManyToMany
|
|||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "users", schema = SCHEMA)
|
||||
class UserEntity : AuditableUuidIdEntity() {
|
||||
@Table(name = "users")
|
||||
class UserEntity: AuditableUuidIdEntity() {
|
||||
|
||||
var anonymous: Boolean = true
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ class UserEntity : AuditableUuidIdEntity() {
|
|||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "devices_users",
|
||||
schema = SCHEMA,
|
||||
joinColumns = [JoinColumn(name = "user_id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "device_id")]
|
||||
)
|
||||
|
|
@ -31,14 +29,9 @@ class UserEntity : AuditableUuidIdEntity() {
|
|||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "users_scopes",
|
||||
schema = SCHEMA,
|
||||
joinColumns = [JoinColumn(name = "user_id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "scope_name")]
|
||||
)
|
||||
lateinit var scopes: MutableSet<ScopeEntity>
|
||||
|
||||
fun addScopes(scopes: Collection<ScopeEntity>) {
|
||||
this.scopes.addAll(scopes)
|
||||
}
|
||||
lateinit var scopes: Set<ScopeEntity>
|
||||
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ package ru.touchin.auth.core.user.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.user.dto.enums.IdentifierType
|
||||
import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException
|
||||
import ru.touchin.auth.core.user.models.UserAccountEntity
|
||||
|
|
@ -19,26 +18,9 @@ interface UserAccountRepository: JpaRepository<UserAccountEntity, UUID> {
|
|||
""")
|
||||
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 {
|
||||
return findByUsername(username, identifierType)
|
||||
?: throw UserAccountNotFoundException(username)
|
||||
}
|
||||
|
||||
fun UserAccountRepository.findByUserIdOrThrow(userId: UUID, identifierType: IdentifierType): UserAccountEntity {
|
||||
return findByUserId(userId, identifierType)
|
||||
?: throw UserAccountNotFoundException(userId.toString())
|
||||
}
|
||||
|
|
@ -1,28 +1,17 @@
|
|||
package ru.touchin.auth.core.user.services
|
||||
|
||||
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.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.NewUser
|
||||
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 {
|
||||
|
||||
fun create(newAnonymousUser: NewAnonymousUser): User
|
||||
fun create(newUser: NewUser): User
|
||||
fun get(username: String, identifierType: IdentifierType): User
|
||||
fun getUserAccount(userAccount: GetUserAccount): UserAccount
|
||||
fun getOrNull(username: String, identifierType: IdentifierType): User?
|
||||
fun login(userLogin: UserLogin): User
|
||||
fun logout(userLogout: UserLogout)
|
||||
fun updatePassword(update: UserUpdatePassword)
|
||||
fun setPassword(userSetPassword: UserSetPassword)
|
||||
fun addScopes(addUserScopes: AddUserScopes)
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
package ru.touchin.auth.core.user.services
|
||||
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
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.repository.DeviceRepository
|
||||
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.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.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.exceptions.UserAccountNotFoundException
|
||||
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.repositories.UserAccountRepository
|
||||
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.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.NewUser
|
||||
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
|
||||
class UserCoreServiceImpl(
|
||||
|
|
@ -58,7 +46,7 @@ class UserCoreServiceImpl(
|
|||
val user = UserEntity().apply {
|
||||
anonymous = true
|
||||
devices = hashSetOf(device)
|
||||
scopes = mutableSetOf()
|
||||
scopes = emptySet()
|
||||
}
|
||||
|
||||
return userRepository.save(user)
|
||||
|
|
@ -80,7 +68,7 @@ class UserCoreServiceImpl(
|
|||
.apply {
|
||||
anonymous = false
|
||||
devices = hashSetOf(device)
|
||||
scopes = defaultScopes.toMutableSet()
|
||||
scopes = defaultScopes.toSet()
|
||||
}
|
||||
.also(userRepository::save)
|
||||
|
||||
|
|
@ -112,85 +100,19 @@ class UserCoreServiceImpl(
|
|||
|
||||
val user = userAccount.user
|
||||
.apply {
|
||||
devices = devices + device
|
||||
devices = hashSetOf(device)
|
||||
}
|
||||
.also(userRepository::save)
|
||||
|
||||
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)
|
||||
override fun get(username: String, identifierType: IdentifierType): User {
|
||||
return getOrNull(username, identifierType)
|
||||
?: throw UserAccountNotFoundException(username)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun getUserAccount(userAccount: GetUserAccount): UserAccount {
|
||||
return userAccountRepository.findByUserIdOrThrow(userAccount.userId, userAccount.identifierType)
|
||||
.toDto()
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun getOrNull(username: String, identifierType: IdentifierType): User? {
|
||||
return userAccountRepository.findByUsername(username, identifierType)
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105011900__create_table__devices
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: devices
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: devices
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: devices
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105052300__create_table__users
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: users
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: users
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: users
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105052310__create_table__user_accounts
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: user_accounts
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: user_accounts
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: user_accounts
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105052331__create_table__devices_users
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: devices_users
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: devices_users
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: devices_users
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105081411__create_table__scopes
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: scopes
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: scopes
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: scopes
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105081431__create_table__scope_groups
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: scope_groups
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: scope_groups
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: scope_groups
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105081531__create_table__users_scopes
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: users_scopes
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: users_scopes
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: users_scopes
|
||||
|
|
@ -8,12 +8,12 @@ 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.devices.enums.DevicePlatform
|
||||
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
|
@ -14,8 +14,8 @@ 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 ru.touchin.common.devices.enums.DevicePlatform
|
||||
import java.util.*
|
||||
|
||||
@ActiveProfiles("test")
|
||||
|
|
@ -48,7 +48,7 @@ internal class ScopeRepositoryTest {
|
|||
val savedScope = scopeRepository.findByIdOrThrow(scope.name)
|
||||
|
||||
assertTrue(
|
||||
ReflectionEquals(scope, "createdAt", "users").matches(savedScope)
|
||||
ReflectionEquals(scope, "createdAt").matches(savedScope)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -9,12 +9,12 @@ 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.devices.enums.DevicePlatform
|
||||
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.*
|
||||
|
|
@ -43,7 +43,7 @@ internal class UserRepositoryTest {
|
|||
anonymous = false
|
||||
confirmedAt = ZonedDateTime.now()
|
||||
devices = emptySet()
|
||||
scopes = mutableSetOf()
|
||||
scopes = emptySet()
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ internal class UserRepositoryTest {
|
|||
|
||||
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()
|
||||
.apply {
|
||||
scopes = mutableSetOf(scope)
|
||||
scopes = setOf(scope)
|
||||
}
|
||||
|
||||
userRepository.saveAndFlush(user)
|
||||
|
|
@ -117,7 +117,7 @@ internal class UserRepositoryTest {
|
|||
fun shouldBeErrorIfScopeNew() {
|
||||
val user = UserEntity()
|
||||
.apply {
|
||||
scopes = mutableSetOf(ScopeEntity().apply { name = "admin" })
|
||||
scopes = setOf(ScopeEntity().apply { name = "admin" })
|
||||
}
|
||||
|
||||
assertThrows(DataAccessException::class.java) {
|
||||
|
|
@ -2,7 +2,6 @@ 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.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
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.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.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.enums.IdentifierType
|
||||
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.UserRepository
|
||||
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.NewUser
|
||||
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 java.lang.IllegalArgumentException
|
||||
import java.util.*
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
|
|
@ -184,7 +179,7 @@ internal class UserCoreServiceImplSlowTest {
|
|||
fun loginShouldBeOk() {
|
||||
val device = createDevice()
|
||||
|
||||
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee")
|
||||
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee" )
|
||||
|
||||
val logingUser = userCoreService.login(
|
||||
UserLogin(
|
||||
|
|
@ -287,128 +282,4 @@ internal class UserCoreServiceImplSlowTest {
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.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
|
||||
|
|
@ -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.services.dto.NewAnonymousUser
|
||||
import ru.touchin.auth.core.user.services.dto.NewUser
|
||||
import ru.touchin.common.devices.enums.DevicePlatform
|
||||
import java.util.*
|
||||
|
||||
@ActiveProfiles("test")
|
||||
|
|
@ -2,3 +2,5 @@ databaseChangeLog:
|
|||
- changeset:
|
||||
id: 1
|
||||
author: test
|
||||
- includeAll:
|
||||
path: classpath*:/db/changelog/auth/core
|
||||
|
|
@ -5,9 +5,7 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":security-authorization-server-core"))
|
||||
implementation(project(":security-resource-server-custom-jwt-configuration"))
|
||||
implementation(project(":security-jwt-common"))
|
||||
implementation(project(":auth-core"))
|
||||
|
||||
implementation("com.auth0:java-jwt")
|
||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
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")
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
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
|
||||
|
|
@ -1,33 +1,35 @@
|
|||
package ru.touchin.auth.core.tokens.access.config
|
||||
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.context.annotation.Bean
|
||||
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.security.jwt.configurations.JwtConfiguration
|
||||
import ru.touchin.auth.security.jwt.utils.JwtUtils.getKeySpec
|
||||
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
|
||||
@Import(JwtConfiguration::class)
|
||||
class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProperties) {
|
||||
|
||||
@Bean
|
||||
fun accessTokenSigningAlgorithm(
|
||||
@Qualifier("accessTokenPublicKey")
|
||||
accessTokenPublicKey: RSAPublicKey
|
||||
): Algorithm {
|
||||
fun accessTokenSigningAlgorithm(): Algorithm {
|
||||
return Algorithm.RSA256(
|
||||
accessTokenPublicKey,
|
||||
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)
|
||||
|
|
@ -35,6 +37,22 @@ class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProper
|
|||
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")
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
package ru.touchin.auth.core.tokens.access.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
|
@ -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.AccessTokenRequest
|
||||
|
||||
interface AccessTokenCoreService {
|
||||
interface AccessTokenService {
|
||||
|
||||
fun create(accessTokenRequest: AccessTokenRequest): AccessToken
|
||||
|
||||
|
|
@ -13,10 +13,10 @@ import java.time.ZoneId
|
|||
import java.util.*
|
||||
|
||||
@Service
|
||||
class JwtAccessTokenCoreServiceImpl(
|
||||
class JwtAccessTokenServiceImpl(
|
||||
private val accessTokenProperties: AccessTokenProperties,
|
||||
private val accessTokenSigningAlgorithm: Algorithm
|
||||
) : AccessTokenCoreService {
|
||||
) : AccessTokenService {
|
||||
|
||||
private fun sign(builder: JWTCreator.Builder) = builder.sign(accessTokenSigningAlgorithm)
|
||||
|
||||
|
|
@ -6,6 +6,5 @@ import java.time.ZonedDateTime
|
|||
data class RefreshToken(
|
||||
val value: String,
|
||||
val expiresAt: ZonedDateTime,
|
||||
val usedAt: ZonedDateTime?,
|
||||
val user: User,
|
||||
)
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
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.scope.models.ScopeEntity
|
||||
import ru.touchin.auth.core.tokens.refresh.exceptions.RefreshTokenExpiredException
|
||||
|
|
@ -18,15 +15,13 @@ import javax.persistence.ManyToOne
|
|||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "refresh_tokens", schema = SCHEMA)
|
||||
@Table(name = "refresh_tokens")
|
||||
class RefreshTokenEntity : AuditableUuidIdEntity() {
|
||||
|
||||
lateinit var value: String
|
||||
|
||||
lateinit var expiresAt: ZonedDateTime
|
||||
|
||||
var usedAt: ZonedDateTime? = null
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
lateinit var user: UserEntity
|
||||
|
|
@ -38,14 +33,13 @@ class RefreshTokenEntity : AuditableUuidIdEntity() {
|
|||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "refresh_token_scopes",
|
||||
schema = SCHEMA,
|
||||
joinColumns = [JoinColumn(name = "refresh_token_id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "scope_name")]
|
||||
)
|
||||
lateinit var scopes: Set<ScopeEntity>
|
||||
|
||||
fun validate(): RefreshTokenEntity = this.apply {
|
||||
if (expiresAt.isExpired() || usedAt != null) {
|
||||
if (expiresAt.isExpired()) {
|
||||
throw RefreshTokenExpiredException(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.services.dto.NewRefreshToken
|
||||
|
||||
interface RefreshTokenCoreService {
|
||||
interface RefreshTokenService {
|
||||
|
||||
fun get(value: String): RefreshToken
|
||||
fun create(token: NewRefreshToken): RefreshToken
|
||||
fun refresh(value: String): RefreshToken
|
||||
|
||||
}
|
||||
|
|
@ -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.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
|
||||
|
|
@ -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.services.dto.NewRefreshToken
|
||||
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.security.hash.HashUtils
|
||||
import ru.touchin.common.security.hash.HashUtils.calculateHash
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Service
|
||||
class RefreshTokenCoreServiceImpl(
|
||||
class RefreshTokenServiceImpl(
|
||||
private val refreshTokenProperties: RefreshTokenProperties,
|
||||
private val refreshTokenRepository: RefreshTokenRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val deviceRepository: DeviceRepository,
|
||||
private val scopeRepository: ScopeRepository,
|
||||
) : RefreshTokenCoreService {
|
||||
) : RefreshTokenService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
override fun get(value: String): RefreshToken {
|
||||
val valueHash = getTokenHash(value)
|
||||
|
||||
return refreshTokenRepository.findByValueOrThrow(valueHash)
|
||||
.toDto(value)
|
||||
return refreshTokenRepository.findByValueOrThrow(value)
|
||||
.toDto()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -44,44 +39,17 @@ class RefreshTokenCoreServiceImpl(
|
|||
val user = userRepository.findByIdOrThrow(token.userId)
|
||||
val device = token.deviceId?.let(deviceRepository::findByIdOrNull)
|
||||
val scopes = scopeRepository.findAllById(token.scopes.map(Scope::name))
|
||||
val value = generateTokenValue()
|
||||
|
||||
val model = RefreshTokenEntity().apply {
|
||||
expiresAt = getExpirationDate()
|
||||
this.value = getTokenHash(value)
|
||||
value = generateTokenValue()
|
||||
this.user = user
|
||||
this.device = device
|
||||
this.scopes = scopes.toSet()
|
||||
}
|
||||
|
||||
return refreshTokenRepository.save(model)
|
||||
.toDto(value)
|
||||
}
|
||||
|
||||
@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)
|
||||
.toDto()
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
fun RefreshTokenEntity.toDto(rawValue: String): RefreshToken {
|
||||
fun RefreshTokenEntity.toDto(): RefreshToken {
|
||||
val device = device?.toDto()
|
||||
|
||||
return RefreshToken(
|
||||
value = rawValue,
|
||||
value = value,
|
||||
expiresAt = expiresAt,
|
||||
usedAt = usedAt,
|
||||
user = user.toDto(device)
|
||||
)
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105201851__create_table__refresh_tokens
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: refresh_tokens
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: refresh_tokens
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: refresh_tokens
|
||||
|
|
@ -3,10 +3,10 @@ databaseChangeLog:
|
|||
id: 202105201901__create_table__refresh_token_scopes
|
||||
author: touchin
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: refresh_token_scopes
|
||||
onFail: MARK_RAN
|
||||
not:
|
||||
tableExists:
|
||||
tableName: refresh_token_scopes
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: refresh_token_scopes
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
|
||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||
import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("org.springframework.boot") apply false
|
||||
id ("org.springframework.boot") apply false
|
||||
|
||||
// IntelliJ
|
||||
idea
|
||||
|
|
@ -17,9 +15,6 @@ plugins {
|
|||
// A Gradle plugin that provides Maven-like dependency management and exclusions
|
||||
// https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/
|
||||
id("io.spring.dependency-management")
|
||||
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("org.cqfn.diktat.diktat-gradle-plugin")
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
|
@ -35,30 +30,6 @@ allprojects {
|
|||
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 {
|
||||
println("Enabling Kotlin JVM plugin in project ${project.name}...")
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
|
|
@ -69,26 +40,6 @@ subprojects {
|
|||
println("Enabling Spring Boot Dependency Management in project ${project.name}...")
|
||||
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> {
|
||||
imports {
|
||||
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-jackson:0.1.5")
|
||||
|
||||
dependency("org.testcontainers:testcontainers:1.18.3")
|
||||
dependency("org.testcontainers:postgresql:1.18.3")
|
||||
dependency("org.testcontainers:junit-jupiter:1.18.3")
|
||||
dependency("org.testcontainers:testcontainers:1.15.1")
|
||||
dependency("org.testcontainers:postgresql:1.15.1")
|
||||
dependency("org.testcontainers:junit-jupiter:1.15.1")
|
||||
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-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("org.mockito:mockito-inline:3.11.0")
|
||||
|
||||
|
|
@ -121,19 +68,13 @@ subprojects {
|
|||
dependency("org.locationtech.spatial4j:spatial4j:0.8")
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// use for @ConstructorBinding
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
implementation ("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaActionMismatchException(
|
||||
action: String
|
||||
) : CommonException("invalid captcha action $action")
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaResponseMissingException : CommonException("missing captcha response header")
|
||||
|
|
@ -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")
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaSiteVerifyFailureException(
|
||||
description: String?
|
||||
) : CommonException(description)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package ru.touchin.captcha.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class CaptchaUnknownActionException(
|
||||
action: String
|
||||
) : CommonException("unknown captcha action $action")
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package ru.touchin.captcha.services
|
||||
|
||||
import ru.touchin.captcha.dto.CaptchaVerificationResult
|
||||
|
||||
interface CaptchaService {
|
||||
|
||||
fun verify(response: String): CaptchaVerificationResult
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
plugins {
|
||||
id("kotlin")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation("com.tngtech.archunit:archunit")
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue