diff --git a/.env b/.env index 6f094db..9b8243a 100644 --- a/.env +++ b/.env @@ -4,6 +4,7 @@ DATABASE_URL=jdbc:postgresql:dc-project APP_PORT=8080 OPENAPI_PORT=8181 +SONARQUBE_PORT=9000 ELASTIC_REST=9200 ELASTIC_NODES=9300 @@ -13,4 +14,12 @@ DB_HOST=db DB_PORT=5432 DB_NAME=dc-project DB_USER=dc-project -DB_PWD=dc-project \ No newline at end of file +DB_PWD=dc-project + +REDIS_PORT=6379 +REDIS_CONNECTION=redis://localhost:6379 +REDIS_COMMANDER_PORT=8081 + +RABBITMQ_PORT=5672 +RABBITMQ_CONNECTION=amqp://localhost:5672 +RABBITMQ_MANAGEMENT_PORT=15672 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index eaeafd5..84cee6a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ import org.owasp.dependencycheck.reporting.ReportGenerator val ktor_version: String by project val kotlin_version: String by project +val coroutinesVersion: String by project val logback_version: String by project val koinVersion: String by project val postgresjson_version: String by project @@ -66,6 +67,8 @@ repositories { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}") implementation("io.ktor:ktor-server-jetty:$ktor_version") implementation("ch.qos.logback:logback-classic:$logback_version") implementation("io.ktor:ktor-server-core:$ktor_version") @@ -83,6 +86,8 @@ dependencies { implementation("com.github.jasync-sql:jasync-postgresql:1.0.7") implementation("fr.postgresjson:postgresjson:$postgresjson_version") implementation("com.sendgrid:sendgrid-java:4.4.1") + implementation("io.lettuce:lettuce-core:5.2.2.RELEASE") + implementation("com.rabbitmq:amqp-client:5.8.0") testImplementation("io.ktor:ktor-server-tests:$ktor_version") testImplementation("io.ktor:ktor-client-mock:$ktor_version") diff --git a/docker-compose.yml b/docker-compose.yml index 2337801..ce78543 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: image: sonarqube restart: always ports: - - 9000:9000 + - ${SONARQUBE_PORT}:9000 openapi: container_name: openapi_${NAME} @@ -18,6 +18,21 @@ services: environment: URL: "http://localhost:8080" + rabbitmq: + container_name: rabbitmq_${NAME} + image: rabbitmq:management-alpine + restart: always + ports: + - ${RABBITMQ_PORT}:5672 + - ${RABBITMQ_MANAGEMENT_PORT}:15672 + + redis: + container_name: redis_${NAME} + image: redis:6.0-rc-alpine + restart: always + ports: + - ${REDIS_PORT}:6379 + app: container_name: app_${NAME} build: @@ -29,9 +44,13 @@ services: environment: DB_HOST: db SEND_GRID_KEY: ${SEND_GRID_KEY} + REDIS_CONNECTION: redis://redis:6379 + RABBITMQ_CONNECTION: amqp://rabbitmq:5671 depends_on: - elasticsearch - db + - redis + - rabbitmq elasticsearch: container_name: elasticsearch_${NAME} diff --git a/gradle.properties b/gradle.properties index 79bd462..27d072a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ ktor_version=1.2.2 kotlin.code.style=official kotlin_version=1.3.40 +coroutinesVersion=1.3.3 logback_version=1.2.1 postgresjson_version=0.1 koinVersion=2.0.1 diff --git a/src/main/kotlin/fr/dcproject/Application.kt b/src/main/kotlin/fr/dcproject/Application.kt index 7fda334..d1ac9a9 100644 --- a/src/main/kotlin/fr/dcproject/Application.kt +++ b/src/main/kotlin/fr/dcproject/Application.kt @@ -7,8 +7,12 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.joda.JodaModule import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException +import com.rabbitmq.client.ConnectionFactory import fr.dcproject.Env.PROD import fr.dcproject.entity.* +import fr.dcproject.event.EntityEvent +import fr.dcproject.event.EventNotification +import fr.dcproject.event.publisher.Publisher import fr.dcproject.routes.* import fr.dcproject.security.voter.* import fr.postgresjson.migration.Migrations @@ -27,7 +31,9 @@ import io.ktor.jackson.jackson import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Locations import io.ktor.response.respond +import io.ktor.response.respondText import io.ktor.routing.Routing +import io.ktor.routing.get import io.ktor.util.KtorExperimentalAPI import org.eclipse.jetty.util.log.Slf4jLog import org.koin.ktor.ext.Koin @@ -148,6 +154,25 @@ fun Application.module(env: Env = PROD) { ) } + install(EventNotification) { + /* Config Rabbit */ + val exchangeName = config.exchangeNotificationName + get().newConnection().use { connection -> connection.createChannel().use { channel -> + channel.queueDeclare("sse", true, false, false, null) + channel.queueDeclare("email", true, false, false, null) + channel.exchangeDeclare(exchangeName, "direct") + channel.queueBind("sse", exchangeName, "") + channel.queueBind("email", exchangeName, "") + }} + + /* Declare publisher on event */ + val publisher = Publisher(get(), get()) + subscribe(EntityEvent.Type.UPDATE_ARTICLE.event) { + println("Article is updated ${it.target.id}") + publisher.publish(it) + } + } + install(Authentication) { /** * Setup the JWT authentication to be used in [Routing]. @@ -200,6 +225,20 @@ fun Application.module(env: Env = PROD) { opinionArticle(get()) opinionChoice(get()) definition() + get("/sse") { +// environment.monitor.raise(EntityEvent.Type.UPDATE_ARTICLE.event, ArticleUpdate(ArticleRef())) +// val redis = this@authenticate.getKoin().get>() +// redis.set("key", "test").awaitSingle() +// redis.lpush("list", "test2").asFlow().map { +// println(it) +// }.collect() +// redis.get("key").asFlow().collect { println(it) } +// redis.rpop("list").asFlow().collect { +// println(it) +// call.respondText { it } +// } + call.respondText("OK") + } } } diff --git a/src/main/kotlin/fr/dcproject/Configuration.kt b/src/main/kotlin/fr/dcproject/Configuration.kt index 7592600..25686f0 100644 --- a/src/main/kotlin/fr/dcproject/Configuration.kt +++ b/src/main/kotlin/fr/dcproject/Configuration.kt @@ -26,7 +26,9 @@ class Config { var username: String = config.getString("db.username") var password: String = config.getString("db.password") val port: Int = config.getInt("db.port") - + val redis: String = config.getString("redis.connection") + val rabbitmq: String = config.getString("rabbitmq.connection") + val exchangeNotificationName = "notification" val sendGridKey: String = config.getString("mail.sendGrid.key") } diff --git a/src/main/kotlin/fr/dcproject/Module.kt b/src/main/kotlin/fr/dcproject/Module.kt index 3a2ee3a..63c0fde 100644 --- a/src/main/kotlin/fr/dcproject/Module.kt +++ b/src/main/kotlin/fr/dcproject/Module.kt @@ -1,11 +1,21 @@ package fr.dcproject +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategy +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.datatype.joda.JodaModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.rabbitmq.client.ConnectionFactory import fr.dcproject.messages.Mailer import fr.dcproject.messages.SsoManager import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Requester import fr.postgresjson.migration.Migrations import io.ktor.util.KtorExperimentalAPI +import io.lettuce.core.RedisClient +import io.lettuce.core.api.reactive.RedisReactiveCommands import org.koin.dsl.module import fr.dcproject.repository.Article as ArticleRepository import fr.dcproject.repository.Citizen as CitizenRepository @@ -39,6 +49,25 @@ val Module = module { ) } + single> { + RedisClient.create(config.redis).connect()?.reactive() ?: error("Unable to connect to redis") + } + + single { + ConnectionFactory().apply { setUri(config.rabbitmq) } + } + + single { + jacksonObjectMapper().apply { + registerModule(SimpleModule()) + propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE + + registerModule(JodaModule()) + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + } + } + single { Requester.RequesterFactory( connection = get(), diff --git a/src/main/kotlin/fr/dcproject/event/EventNotification.kt b/src/main/kotlin/fr/dcproject/event/EventNotification.kt new file mode 100644 index 0000000..f042dd3 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/event/EventNotification.kt @@ -0,0 +1,54 @@ +package fr.dcproject.event + +import fr.dcproject.entity.Article +import fr.postgresjson.entity.immutable.UuidEntity +import io.ktor.application.* +import io.ktor.util.AttributeKey +import io.ktor.util.KtorExperimentalAPI +import kotlinx.coroutines.DisposableHandle +import org.joda.time.DateTime + +abstract class Notification( + val type: String, + val createdAt: DateTime = DateTime.now() +) +abstract class EntityEvent( + val target: UuidEntity, + type: String, + val action: String +) : Notification(type) { + enum class Type(val event: EventDefinition) { + UPDATE_ARTICLE(EventDefinition()) + } +} + +class ArticleUpdate( + target: Article +) : EntityEvent(target, "article", "update") + +/** + * Installation Class + */ +class EventNotification { + class Configuration(private val monitor: ApplicationEvents) { + private val subscribers = mutableListOf() + fun subscribe(definition: EventDefinition, handler: EventHandler): DisposableHandle { + return monitor.subscribe(definition, handler).also { + subscribers.add(it) + } + } + } + + companion object Feature : ApplicationFeature { + override val key = AttributeKey("EventNotification") + + @KtorExperimentalAPI + override fun install( + pipeline: Application, + configure: Configuration.() -> Unit + ): EventNotification { + Configuration(pipeline.environment.monitor).apply(configure) + return EventNotification() + } + } +} diff --git a/src/main/kotlin/fr/dcproject/event/publisher/Publisher.kt b/src/main/kotlin/fr/dcproject/event/publisher/Publisher.kt new file mode 100644 index 0000000..e181dc6 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/event/publisher/Publisher.kt @@ -0,0 +1,30 @@ +package fr.dcproject.event.publisher + +import com.fasterxml.jackson.databind.ObjectMapper +import com.rabbitmq.client.ConnectionFactory +import fr.dcproject.config +import fr.dcproject.event.EntityEvent +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class Publisher( + private val mapper: ObjectMapper, + private val factory: ConnectionFactory, + private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName) +) { + fun publish(it: T): Job { + return GlobalScope.launch { + factory.newConnection().use { connection -> connection.createChannel().use { channel -> + channel.basicPublish(config.exchangeNotificationName, "", null, it.serialize().toByteArray()) + logger.debug("Publish message ${it.target.id}") + } } + } + } + + private fun EntityEvent.serialize(): String { + return mapper.writeValueAsString(this) ?: error("Unable tu serialize message") + } +} diff --git a/src/main/kotlin/fr/dcproject/routes/Article.kt b/src/main/kotlin/fr/dcproject/routes/Article.kt index a16b07d..b041e56 100644 --- a/src/main/kotlin/fr/dcproject/routes/Article.kt +++ b/src/main/kotlin/fr/dcproject/routes/Article.kt @@ -1,11 +1,14 @@ package fr.dcproject.routes import fr.dcproject.citizen +import fr.dcproject.event.ArticleUpdate +import fr.dcproject.event.EntityEvent import fr.dcproject.repository.Article.Filter import fr.dcproject.security.voter.ArticleVoter.Action.CREATE import fr.dcproject.security.voter.ArticleVoter.Action.VIEW import fr.dcproject.security.voter.assertCan import fr.postgresjson.repository.RepositoryI +import io.ktor.application.application import io.ktor.application.call import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location @@ -83,6 +86,7 @@ fun Route.article(repo: ArticleRepository) { assertCan(CREATE, article) repo.upsert(article) + application.environment.monitor.raise(EntityEvent.Type.UPDATE_ARTICLE.event, ArticleUpdate(article)) call.respond(article) } diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 571f7f7..b3fa396 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -22,6 +22,16 @@ db { port = 5432 } +redis { + connection = "redis://localhost:6379" + connection = ${?REDIS_CONNECTION} +} + +rabbitmq { + connection = "amqp://localhost:5672" + connection = ${?RABBITMQ_CONNECTION} +} + mail { sendGrid { key = ${?SEND_GRID_KEY} diff --git a/src/main/resources/openapi.yaml b/src/main/resources/openapi.yaml index 4e904f7..1ef9f36 100644 --- a/src/main/resources/openapi.yaml +++ b/src/main/resources/openapi.yaml @@ -1132,7 +1132,7 @@ components: required: true example: Lorem upsum... - descritption: + description: type: string required: true example: