Skip to content

Commit d074d5b

Browse files
authored
feat/qg-290: реализованы уведомления о заполнении расписания (#292)
2 parents b1373fe + 2c93851 commit d074d5b

File tree

106 files changed

+2435
-375
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+2435
-375
lines changed

app/build.gradle.kts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import com.gorylenko.GenerateGitPropertiesTask
12
import kotlinx.kover.gradle.plugin.dsl.AggregationType
23
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
34
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
5+
import java.util.*
46

57
plugins {
68
alias(libs.plugins.kotlin.spring)
@@ -43,6 +45,8 @@ dependencies {
4345
implementation(libs.google.oauth.client)
4446
implementation(platform(libs.google.auth.bom))
4547
implementation("com.google.auth:google-auth-library-oauth2-http")
48+
implementation(libs.web.push)
49+
implementation(libs.bouncycastle)
4650

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

@@ -224,3 +228,20 @@ val compileKotlin: KotlinCompile by tasks
224228
compileKotlin.compilerOptions {
225229
freeCompilerArgs.set(listOf("-Xannotation-default-target=param-property"))
226230
}
231+
232+
val generateGitProperties = tasks.named<GenerateGitPropertiesTask>("generateGitProperties")
233+
234+
val gitHash: Provider<String> = generateGitProperties.map { task ->
235+
val props = Properties()
236+
task.output.get().asFile.inputStream().use(props::load)
237+
props.getProperty("git.commit.id.abbrev")
238+
}
239+
240+
tasks.processResources {
241+
dependsOn(generateGitProperties)
242+
filesMatching("static/sw.js") {
243+
expand(
244+
"APP_VERSION" to gitHash.get()
245+
)
246+
}
247+
}

app/src/main/kotlin/pro/azhidkov/platform/java/sql/ResultSetExt.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ inline operator fun <reified T> ResultSet.get(colName: String): T = when (T::cla
1212
Duration::class -> (this.getObject(colName) as PGInterval).toDuration()
1313
List::class -> (this.getArray(colName).array as Array<*>).toList()
1414
else -> this.getObject(colName)
15-
} as T
15+
} as T
16+
17+
inline operator fun <reified T> ResultSet.get(colIdx: Int): T =
18+
this.metaData.getColumnName(colIdx)
19+
.let { colName -> this[colName] }
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package pro.azhidkov.platform.spring.sdj.converters
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import org.postgresql.util.PGobject
5+
import org.springframework.core.convert.converter.Converter
6+
import org.springframework.data.convert.ReadingConverter
7+
import org.springframework.data.convert.WritingConverter
8+
import kotlin.reflect.KClass
9+
10+
11+
@WritingConverter
12+
fun interface ObjectToJsonbWriter<T : Any> : Converter<T, PGobject>
13+
14+
@ReadingConverter
15+
fun interface JsonbToObjectReader<T : Any> : Converter<PGobject, T>
16+
17+
abstract class JacksonObjectToJsonbWriter<T : Any>(
18+
private val objectMapper: ObjectMapper
19+
) : ObjectToJsonbWriter<T> {
20+
21+
override fun convert(source: T): PGobject =
22+
source.let {
23+
PGobject().apply {
24+
type = "jsonb"
25+
value = objectMapper.writeValueAsString(source)
26+
}
27+
}
28+
29+
}
30+
31+
@ReadingConverter
32+
abstract class JacksonJsonbToObjectReader<T : Any>(
33+
private val objectMapper: ObjectMapper,
34+
private val type: KClass<T>
35+
) : JsonbToObjectReader<T> {
36+
37+
override fun convert(source: PGobject): T? =
38+
if (source.isNull) {
39+
null
40+
} else {
41+
objectMapper.readValue(source.value!!, type.java)
42+
}
43+
44+
}
45+
46+
object ObjectToJsonbConverters {
47+
48+
inline fun <reified T : Any> convertersFor(objectMapper: ObjectMapper) =
49+
setOf(
50+
object : JacksonObjectToJsonbWriter<T>(objectMapper) {},
51+
object : JacksonJsonbToObjectReader<T>(objectMapper, T::class) {},
52+
)
53+
54+
}

app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/hydration/AggregateReferenceTarget.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,24 @@ data class AggregateReferenceTarget<T : Identifiable<ID>, ID : Any>(
1212
@JsonIgnore
1313
override fun getId(): ID = entity.id
1414

15+
override fun equals(other: Any?): Boolean {
16+
if (this === other) return true
17+
if (other !is AggregateReference<*, *>) return false
18+
19+
return if (other is AggregateReferenceTarget<*, *>) {
20+
entity == other.entity
21+
} else {
22+
getId() == other.getId()
23+
}
24+
}
25+
26+
override fun hashCode(): Int {
27+
return entity.id.hashCode()
28+
}
29+
1530
}
1631

32+
@Suppress("UNCHECKED_CAST")
1733
fun <R : AggregateReference<T, ID>?, ID : Any, T : Identifiable<ID>> R.resolveOrThrow(): T =
1834
(this as? AggregateReferenceTarget<T, ID>)?.entity
1935
?: error("$this is not instance of AggregateReferenceTarget")

app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/hydration/Identifiable.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ interface Identifiable<T : Any> {
99

1010
}
1111

12-
fun <E : Identifiable<T>, T : Any> E.ref(): AggregateReference<E, T> = AggregateReferenceTarget(this)
12+
fun <E : Identifiable<T>, T : Any> E.ref(): AggregateReference<E, T> =
13+
AggregateReferenceTarget(this)

app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/query/QueryBuilder.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ class QueryBuilder {
5050
}
5151
}
5252

53+
infix fun <T : Any> KProperty1<*, T?>.`in`(value: Iterable<T>) {
54+
criteria.addLast(Criteria.where(this.name).`in`(value))
55+
}
56+
5357
fun build() = Query.query(toCriteriaDefinition())
5458

5559
private fun toCriteriaDefinition() =
@@ -74,4 +78,4 @@ fun query(body: QueryBuilder.() -> Unit): Query {
7478
val builder = QueryBuilder()
7579
builder.body()
7680
return builder.build()
77-
}
81+
}

app/src/main/kotlin/pro/qyoga/app/QYogaApp.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import pro.qyoga.core.users.UsersConfig
1616
import pro.qyoga.i9ns.calendars.google.GoogleCalendarConf
1717
import pro.qyoga.i9ns.calendars.ical.ICalCalendarsConfig
1818
import pro.qyoga.i9ns.email.EmailsConfig
19+
import pro.qyoga.i9ns.pushes.web.WebPushesConf
1920
import pro.qyoga.infra.auth.AuthConfig
2021
import pro.qyoga.infra.cache.CacheConf
2122
import pro.qyoga.infra.db.SdjConfig
@@ -42,6 +43,7 @@ import pro.qyoga.tech.captcha.CaptchaConf
4243
EmailsConfig::class,
4344
ICalCalendarsConfig::class,
4445
GoogleCalendarConf::class,
46+
WebPushesConf::class,
4547

4648
// Tech
4749
CaptchaConf::class,

app/src/main/kotlin/pro/qyoga/app/infra/WebSecurityConfig.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,36 @@ class WebSecurityConfig(
5050
requests
5151
// Therapist
5252
.requestMatchers("/therapist/**").hasAnyAuthority(Role.ROLE_THERAPIST.toString())
53+
.requestMatchers(HttpMethod.POST, "/pushes/web/subscriptions")
54+
.hasAnyAuthority(Role.ROLE_THERAPIST.toString())
55+
.requestMatchers(HttpMethod.DELETE, "/pushes/web/subscriptions/*")
56+
.hasAnyAuthority(Role.ROLE_THERAPIST.toString())
5357

5458
// Public
5559
.requestMatchers(
5660
HttpMethod.GET,
5761
"/",
62+
63+
"/manifest.json",
64+
"/sw.js",
5865
"/offline.html",
66+
5967
"/privacy-policy.html",
60-
"/manifest.json",
68+
6169
"/register",
70+
6271
"/oauth2/**",
72+
73+
"/pushes/web/public-key",
74+
6375
"/components/**",
76+
6477
"/styles/**",
6578
"/img/**",
6679
"/js/**",
6780
"/fonts/**",
6881
"/vendor/**",
82+
6983
"/test/*"
7084
)
7185
.permitAll()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package pro.qyoga.app.publc.pushes.web
2+
3+
import org.springframework.http.CacheControl
4+
import org.springframework.http.MediaType
5+
import org.springframework.http.ResponseEntity
6+
import org.springframework.web.bind.annotation.GetMapping
7+
import org.springframework.web.bind.annotation.RestController
8+
import pro.qyoga.i9ns.pushes.web.WebPushesConfProps
9+
import java.time.Duration
10+
11+
12+
@RestController
13+
class PushesPublicKeyController(
14+
private val webPushesConfProps: WebPushesConfProps
15+
) {
16+
17+
@GetMapping(PUBLIC_KEY_PATH, produces = [MediaType.TEXT_PLAIN_VALUE])
18+
fun publicKey(): ResponseEntity<String> {
19+
return ResponseEntity.ok()
20+
.cacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePublic())
21+
.body(webPushesConfProps.publicKey)
22+
}
23+
24+
companion object {
25+
26+
const val PUBLIC_KEY_PATH = "/pushes/web/public-key"
27+
28+
}
29+
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package pro.qyoga.app.pushes.web
2+
3+
import org.springframework.stereotype.Component
4+
import pro.qyoga.core.appointments.notifications.fill_schedule.FillScheduleNotificationsSettings
5+
import pro.qyoga.core.appointments.notifications.fill_schedule.FillScheduleNotificationsSettingsRepo
6+
import pro.qyoga.core.users.therapists.TherapistRef
7+
import pro.qyoga.i9ns.pushes.web.WebPushSubscriptionsRepo
8+
import pro.qyoga.i9ns.pushes.web.model.TherapistWebPushSubscription
9+
import pro.qyoga.i9ns.pushes.web.model.WebPushSubscription
10+
11+
12+
@Component
13+
class RegisterSubscriptionOp(
14+
private val webPushSubscriptionsRepo: WebPushSubscriptionsRepo,
15+
private val fillScheduleNotificationsSettingsRepo: FillScheduleNotificationsSettingsRepo
16+
) : (TherapistRef, WebPushSubscription) -> Unit {
17+
18+
override fun invoke(
19+
therapistRef: TherapistRef,
20+
subscription: WebPushSubscription,
21+
) {
22+
webPushSubscriptionsRepo.addSubscription(TherapistWebPushSubscription(therapistRef, subscription))
23+
fillScheduleNotificationsSettingsRepo.createIfNotExists(
24+
FillScheduleNotificationsSettings.defaultSettingsFor(therapistRef)
25+
)
26+
}
27+
28+
}

0 commit comments

Comments
 (0)