Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
79b0246
feat/qg-253(WIP): задышали авторизация в гугле и получение данных кал…
d-r-q Apr 10, 2025
11c72a3
tests/qg-253: webTestClient вынесен в глобальную переменную
d-r-q Jul 31, 2025
4a48eb8
tests/qg-253: дедуплицирована логика определения базового урла сервиса
d-r-q Aug 1, 2025
18fe797
tests/qg-253: в QYogaAppIntegrationBaseKoTest добавлена конфигурация …
d-r-q Aug 1, 2025
45f46bb
tests/qg-253: WebTestClient переведён на работу по ХТТП
d-r-q Aug 2, 2025
98f9c6c
tests/qg-253: работа с WireMock переведена на Java DSL
d-r-q Aug 18, 2025
3bee7e4
feat/qg-253: WIP: реализовано первое приближение аутентификации в Google
d-r-q Sep 11, 2025
ba181b9
feat/qg-253: WIP: реализовано сохранение данных Google-аккаунтов в БД
d-r-q Sep 11, 2025
3759356
feat/qg-253: WIP: реализовано отображение аккаунтов и календарей в мо…
d-r-q Sep 11, 2025
d233a71
feat/qg-253: WIP: добавлено кэширование сервисов Google календаря
d-r-q Sep 11, 2025
e118419
feat/qg-253: WIP: добавлено добавлено автооткрытие модалки настроек к…
d-r-q Sep 12, 2025
2b4f421
feat/qg-253: WIP: реализовано сохранение флага отображения Google-кал…
d-r-q Sep 14, 2025
f03c728
feat/qg-253: WIP: в календари добавлены ссылка на аккаунты
d-r-q Sep 14, 2025
88e8b0a
feat/qg-253: WIP: добавлен учёт флага calendar.shouldBeShown при выбо…
d-r-q Sep 15, 2025
4645144
feat/qg-253: WIP: логика работы с GoogleCalendar инкапсулирована в Go…
d-r-q Sep 16, 2025
d3dac2a
feat/qg-253: WIP: события в расписании из Google календаря приведены …
d-r-q Sep 16, 2025
f2a06c0
fix/qg-253: WIP: GoogleAccount.refreshToken переведён на CharArray
d-r-q Sep 16, 2025
27aeda2
feat/qg-253: WIP: добавлена обработка ошибки получения Google Calendars
d-r-q Sep 17, 2025
58f3650
feat/qg-253: WIP: добавлена обработка ошибки получения событий Google…
d-r-q Sep 17, 2025
f07e106
feat/qg-253: WIP: исправлено создание приёма по Google-эвенту
d-r-q Sep 19, 2025
ab69bb1
feat/qg-253: WIP: исправлено предзаполненное время приёма созаднного …
d-r-q Sep 21, 2025
50ef256
refactor/qg-253: интеграция с Google Calendars перенесена в i9ns
d-r-q Sep 22, 2025
9a0844e
refactor/qg-253: интеграция с ical-календарями перенесена в i9ns
d-r-q Sep 22, 2025
a404de7
refactor/qg-253: пачка мелкой полировки кода
d-r-q Sep 25, 2025
18e3e73
refactor/qg-253: удалена реализация AppointmentsRepo-ом CalendarsService
d-r-q Sep 25, 2025
f469cc1
test/qg-253: слой интеграций добавлен в тест архитектуры
d-r-q Sep 25, 2025
15eed39
refactor/qg-253: типы календарей переведены со строк на типы
d-r-q Sep 25, 2025
386e6f2
refactor/qg-253: поиск CalendarItem по строковому идентификатору инка…
d-r-q Sep 25, 2025
4c58c0e
tests/qg-253: Link переведён на матчинг урлов через UriTemplate
d-r-q Sep 26, 2025
85ee3b9
refactor/qg-253: SourceItem заменён на URI
d-r-q Sep 26, 2025
16af6e5
refactor/qg-253: LocalizedICalCalendarItem удалён в пользу генерализа…
d-r-q Sep 26, 2025
0b9aca4
feat/qg-253: добавлена политика конфиденциальности
d-r-q Sep 26, 2025
427f406
feat/qg-253: на лендинг добавлена ссылка на политику конфиденциальности
d-r-q Sep 26, 2025
9efe983
refactor/qg-253: гора мелких стилистических, структурных и нейминговы…
d-r-q Sep 28, 2025
7a78acd
refactor/qg-253: упрощён метод поиска событий в гугл календарях
d-r-q Sep 28, 2025
c461ee8
refactor/qg-253: загрузка Google аккаунтов календарей из GoogleCalend…
d-r-q Sep 28, 2025
e7da844
tests/qg-253: ScheduleFixture переведён с ClientCardDto на Client
d-r-q Sep 28, 2025
76fd146
tests/qg-253: упрощена структура ScheduleFixture
d-r-q Sep 28, 2025
4688988
tests/qg-253: заведена GoogleCalendarsFixture
d-r-q Sep 29, 2025
da1a795
tests/qg-253: добавлен тест на рендеринг настроек Гугл Календарей с к…
d-r-q Sep 29, 2025
90a8ed9
style/qg-253: Добавлен алиас для ICalCalendarItem<ZonedDateTime>
d-r-q Sep 29, 2025
05418f3
feat/qg-253: добавлена возможность повторного подключения Google акка…
d-r-q Sep 29, 2025
9d0ae1b
refactor/qg-253: ещё горка мелкой полировки
d-r-q Sep 29, 2025
3429ead
Merge branch 'master' into qg-253/google-cal-integration
d-r-q Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ out/
/build/

/deploy/host/secrets.sh
/app/src/main/resources/application-local-dev-secrets.yaml
2 changes: 1 addition & 1 deletion .run/QYogaApp.run.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="QYogaApp" type="SpringBootApplicationConfigurationType"
factoryName="Spring Boot" nameIsGenerated="true">
<option name="ACTIVE_PROFILES" value="local-dev,demo"/>
<option name="ACTIVE_PROFILES" value="local-dev,demo,local-dev-secrets"/>
<option name="ENABLE_LAUNCH_OPTIMIZATION" value="false"/>
<option name="FRAME_DEACTIVATION_UPDATE_POLICY" value="UpdateClassesAndResources"/>
<module name="QYoga.app.main"/>
Expand Down
22 changes: 22 additions & 0 deletions .run/Tests - Calendars Integration.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Tests - Calendars Integration" type="JUnit" factoryName="JUnit">
<module name="QYoga.app.test"/>
<extension name="coverage">
<pattern>
<option name="PATTERN" value="pro.qyoga.tests.cases.app.therapist.clients.*"/>
<option name="ENABLED" value="true"/>
</pattern>
</extension>
<option name="PACKAGE_NAME" value="pro.qyoga.tests.cases.app.therapist.clients"/>
<option name="MAIN_CLASS_NAME" value=""/>
<option name="METHOD_NAME" value=""/>
<option name="TEST_OBJECT" value="pattern"/>
<patterns>
<pattern
testClass="^(pro\.qyoga\.tests\.cases\.app\.therapist\.appointments).*|(pro\.azhidkov\.tests\.cases\.domain\.timezones).*|(pro\.qyoga\.tests\.cases\.i9ns\.calendars).*|(pro\.qyoga\.tests\.cases\.app\.therapist\.calendars).*$"/>
</patterns>
<method v="2">
<option name="Make" enabled="true"/>
</method>
</configuration>
</component>
20 changes: 17 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import kotlinx.kover.gradle.plugin.dsl.AggregationType
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
alias(libs.plugins.kotlin.spring)
Expand All @@ -21,11 +22,14 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation(libs.caffeine)

implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.flywaydb:flyway-database-postgresql")
implementation(libs.jackarta.validation)
implementation(libs.thymeleaf.extras.java8time)
Expand All @@ -34,9 +38,13 @@ dependencies {
implementation(libs.bundles.poi)
implementation(libs.nanocaptcha)
implementation(libs.ical4j)
implementation(libs.google.api.client)
implementation(libs.google.calendar.api)
implementation(libs.google.oauth.client)
implementation(platform(libs.google.auth.bom))
implementation("com.google.auth:google-auth-library-oauth2-http")

developmentOnly("org.springframework.boot:spring-boot-docker-compose")
developmentOnly("org.springframework.boot:spring-boot-devtools")

testFixturesApi("org.springframework.boot:spring-boot-testcontainers")
testFixturesApi(testLibs.kotest.assertions)
Expand All @@ -53,17 +61,19 @@ dependencies {
exclude("org.eclipse.jetty.http2", "http2-server")
}
testFixturesApi(testLibs.wiremock.jetty12)
testFixturesApi(testLibs.wiremock.kotlin)

testFixturesImplementation(kotlin("reflect"))
testFixturesImplementation("org.springframework.boot:spring-boot-starter-data-jdbc")
testFixturesImplementation("org.springframework.boot:spring-boot-starter-web")
testFixturesImplementation("org.springframework.boot:spring-boot-starter-security")
testFixturesImplementation("org.springframework.boot:spring-boot-starter-oauth2-client")
testFixturesImplementation("com.fasterxml.jackson.core:jackson-databind")
testFixturesImplementation(libs.minio)
testFixturesImplementation(libs.ical4j)

testFixturesImplementation("org.springframework.boot:spring-boot-starter-test")
testFixturesImplementation("org.springframework.security:spring-security-test")
testFixturesImplementation("org.springframework.boot:spring-boot-starter-webflux")
testFixturesImplementation("org.testcontainers:junit-jupiter")
testFixturesImplementation("org.testcontainers:postgresql")
testFixturesImplementation(testLibs.testcontainers.minio)
Expand Down Expand Up @@ -221,3 +231,7 @@ configurations.matching { it.name == "detekt" }.all {
}
}
}
val compileKotlin: KotlinCompile by tasks
compileKotlin.compilerOptions {
freeCompilerArgs.set(listOf("-Xannotation-default-target=param-property"))
}
23 changes: 18 additions & 5 deletions app/src/main/kotlin/pro/azhidkov/platform/kotlin/ResultExt.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package pro.azhidkov.platform.kotlin

import kotlin.Result.Companion.failure
import kotlin.Result.Companion.success

inline fun <reified T : Throwable> Result<*>.isFailureOf(): Boolean = this.exceptionOrNull() is T

fun Result<*>.value(): Any? = if (this.isSuccess) this.getOrThrow() else this.exceptionOrNull()!!
Expand All @@ -9,7 +12,7 @@ inline fun <R : Any, reified T : Any?> Result<T>.mapSuccessOrNull(transform: (T

@Suppress("UNCHECKED_CAST")
val result = when {
value != null -> Result.success(transform(value))
value != null -> success(transform(value))
else -> this as Result<R>
}
return result
Expand All @@ -20,7 +23,7 @@ inline fun <R : Any, reified T : Any> Result<T>.mapSuccess(transform: (T) -> R):

@Suppress("UNCHECKED_CAST")
val result = when {
value != null -> Result.success(transform(value))
value != null -> success(transform(value))
else -> this as Result<R>
}
return result
Expand All @@ -29,12 +32,22 @@ inline fun <R : Any, reified T : Any> Result<T>.mapSuccess(transform: (T) -> R):
@Suppress("UNCHECKED_CAST")
inline fun <R : Any, reified T : Any?> Result<T>.mapNull(transform: () -> R): Result<R> =
when {
this.isSuccess && this.getOrNull() == null -> Result.success(transform())
this.isSuccess && this.getOrNull() == null -> success(transform())
else -> this as Result<R>
}

inline fun <reified T : Throwable, V : R, R> Result<V>.recoverFailure(block: (T) -> R): Result<R> =
if (this.exceptionOrNull() is T) Result.success(block(this.exceptionOrNull() as T)) else this
if (this.exceptionOrNull() is T) success(block(this.exceptionOrNull() as T)) else this

inline fun <reified T : Throwable, V : R, R> Result<V>.tryRecover(block: (T) -> Result<R>): Result<R> =
if (this.exceptionOrNull() is T) block(this.exceptionOrNull() as T) else this

inline fun <reified T : Throwable, R> Result<R>.mapFailure(block: (T) -> Throwable): Result<R> =
if (this.exceptionOrNull() is T) Result.failure(block(this.exceptionOrNull() as T)) else this
if (this.exceptionOrNull() is T) failure(block(this.exceptionOrNull() as T)) else this

fun <T> tryExecute(eventsRequest: () -> T): Result<T> =
try {
success(eventsRequest())
} catch (e: Exception) {
failure(e)
}
27 changes: 27 additions & 0 deletions app/src/main/kotlin/pro/azhidkov/platform/secrets/SecretChars.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package pro.azhidkov.platform.secrets

data class SecretChars(
private val value: CharArray
) {

fun show() =
String(value)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as SecretChars

return value.contentEquals(other.value)
}

override fun hashCode(): Int {
return value.contentHashCode()
}

override fun toString(): String {
return "<hidden>"
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package pro.azhidkov.platform.spring.jdbc

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.core.convert.support.DefaultConversionService
import org.springframework.jdbc.core.DataClassRowMapper
import org.springframework.jdbc.core.RowMapper
import pro.azhidkov.platform.spring.sdj.converters.PGIntervalToDurationConverter
import pro.azhidkov.platform.spring.sdj.converters.StringToSecretChars
import pro.azhidkov.platform.spring.sdj.converters.UuidToAggregateReferenceConverter


inline fun <reified T> rowMapperFor(objectMapper: ObjectMapper, columnName: String? = null) = RowMapper<T> { rs, _ ->
Expand All @@ -11,4 +16,12 @@ inline fun <reified T> rowMapperFor(objectMapper: ObjectMapper, columnName: Stri
rs.getString(1)
}
json?.let { objectMapper.readValue(it, T::class.java) }
}
}

inline fun <reified T> taDataClassRowMapper() = DataClassRowMapper.newInstance(T::class.java).apply {
conversionService = DefaultConversionService().apply {
addConverter(PGIntervalToDurationConverter())
addConverter(UuidToAggregateReferenceConverter)
addConverter(StringToSecretChars())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pro.azhidkov.platform.spring.sdj.converters

import org.springframework.core.convert.converter.Converter
import org.springframework.data.convert.ReadingConverter
import org.springframework.data.convert.WritingConverter
import pro.azhidkov.platform.secrets.SecretChars

@WritingConverter
class SecretCharsToString : Converter<SecretChars, String> {
override fun convert(source: SecretChars) = source.show()
}

@ReadingConverter
class StringToSecretChars : Converter<String, SecretChars> {
override fun convert(source: String) = SecretChars(source.toCharArray())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package pro.azhidkov.platform.spring.sdj.converters

import org.springframework.core.convert.converter.Converter
import org.springframework.data.jdbc.core.mapping.AggregateReference
import java.util.*

object UuidToAggregateReferenceConverter : Converter<UUID, AggregateReference<*, UUID>> {
override fun convert(source: UUID): AggregateReference<*, UUID> {
return AggregateReference.to<Any, UUID>(source)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ fun <T : Any> JdbcAggregateOperations.hydrate(
entities: Iterable<T>,
fetchSpec: FetchSpec<T>
): List<T> {
if (fetchSpec.propertyFetchSpecs.isEmpty()) {
return (entities as? List<T>) ?: entities.toList()
}

val refs: Map<KProperty1<*, AggregateReference<*, *>?>, Map<Any, Any>> =
fetchSpec.propertyFetchSpecs.filter {
detectRefType(
Expand Down Expand Up @@ -116,4 +120,4 @@ private fun detectRefType(property: KProperty1<*, *>): RefType? = when {

enum class RefType {
SCALAR,
}
}
12 changes: 9 additions & 3 deletions app/src/main/kotlin/pro/qyoga/app/QYogaApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import pro.azhidkov.platform.spring.sdj.ErgoSdjConfig
import pro.qyoga.app.publc.PublicAppConfig
import pro.qyoga.app.therapist.TherapistWebAppConfig
import pro.qyoga.core.appointments.AppointmentsConfig
import pro.qyoga.core.calendar.ical.ICalCalendarsConfig
import pro.qyoga.core.calendar.gateways.CalendarGatewaysConf
import pro.qyoga.core.clients.ClientsConfig
import pro.qyoga.core.survey_forms.SurveyFormsSettingsConfig
import pro.qyoga.core.therapy.TherapyConfig
import pro.qyoga.core.users.UsersConfig
import pro.qyoga.i9ns.calendars.google.GoogleCalendarConf
import pro.qyoga.i9ns.calendars.ical.ICalCalendarsConfig
import pro.qyoga.i9ns.email.EmailsConfig
import pro.qyoga.infra.auth.AuthConfig
import pro.qyoga.infra.cache.CacheConf
import pro.qyoga.infra.db.SdjConfig
import pro.qyoga.infra.minio.MinioConfig
import pro.qyoga.infra.timezones.TimeZonesConfig
Expand All @@ -33,10 +36,12 @@ import pro.qyoga.tech.captcha.CaptchaConf
TherapyConfig::class,
UsersConfig::class,
SurveyFormsSettingsConfig::class,
ICalCalendarsConfig::class,
CalendarGatewaysConf::class,

// I9ns
EmailsConfig::class,
ICalCalendarsConfig::class,
GoogleCalendarConf::class,

// Tech
CaptchaConf::class,
Expand All @@ -49,7 +54,8 @@ import pro.qyoga.tech.captcha.CaptchaConf
ErgoSdjConfig::class,
MinioConfig::class,
FilesStorageConfig::class,
TimeZonesConfig::class
TimeZonesConfig::class,
CacheConf::class
)
@SpringBootApplication
class QYogaApp
Expand Down
14 changes: 12 additions & 2 deletions app/src/main/kotlin/pro/qyoga/app/infra/WebSecurityConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.springframework.security.config.Customizer.withDefaults
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository
Expand Down Expand Up @@ -54,8 +56,10 @@ class WebSecurityConfig(
HttpMethod.GET,
"/",
"/offline.html",
"/privacy-policy.html",
"/manifest.json",
"/register",
"/oauth2/**",
"/components/**",
"/styles/**",
"/img/**",
Expand All @@ -79,6 +83,7 @@ class WebSecurityConfig(
.failureForwardUrl("/error-p")
.permitAll()
}
.oauth2Client(withDefaults())
.logout { logout: LogoutConfigurer<HttpSecurity?> -> logout.permitAll() }
.rememberMe { rememberMeConfigurer ->
rememberMeConfigurer
Expand All @@ -89,11 +94,16 @@ class WebSecurityConfig(
return http.build()
}

@Bean
fun authorizedClientRepository(): OAuth2AuthorizedClientRepository {
return HttpSessionOAuth2AuthorizedClientRepository()
}

@Bean
fun tokenRepository(): PersistentTokenRepository {
val jdbcTokenRepositoryImpl = JdbcTokenRepositoryImpl()
jdbcTokenRepositoryImpl.setDataSource(dataSource)
jdbcTokenRepositoryImpl.dataSource = dataSource
return jdbcTokenRepositoryImpl
}

}
}
Loading
Loading