1 Commits
fix ... doc

Author SHA1 Message Date
4bb458e8d6 Add developer documentation fo create action 2021-04-09 00:20:58 +02:00
179 changed files with 1241 additions and 4137 deletions

View File

@@ -4,9 +4,6 @@
name: Tests
on:
push:
branches:
- 'master'
pull_request:
branches:
- 'master'
@@ -21,10 +18,6 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 11
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
- name: Cache Gradle packages
uses: actions/cache@v2
@@ -36,17 +29,26 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Build
uses: gradle/gradle-build-action@v2
uses: eskatos/gradle-command-action@v1
with:
gradle-version: '7.4'
gradle-version: 6.8
arguments: build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt
- name: Cleanup Gradle Cache
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties
- name: processResources
run: gradle processResources
- name: processTestResources
uses: gradle/gradle-build-action@v2
uses: eskatos/gradle-command-action@v1
with:
gradle-version: '7.4'
gradle-version: 6.8
arguments: processResources
- name: processTestResources
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: processResources
- uses: actions/upload-artifact@v2
with:
@@ -67,17 +69,10 @@ jobs:
with:
name: Build
path: build
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testSqlComposeUp
- name: TestSql
uses: gradle/gradle-build-action@v2
uses: eskatos/gradle-command-action@v1
with:
gradle-version: '7.4'
gradle-version: 6.8
arguments: testSql
test:
@@ -86,58 +81,37 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/download-artifact@v2
with:
name: Build
path: build
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testComposeUp
- name: Test
uses: gradle/gradle-build-action@v2
uses: eskatos/gradle-command-action@v1
with:
gradle-version: '7.4'
arguments: test
gradle-version: 6.8
arguments: test -x testSql
- name: Coverage
uses: gradle/gradle-build-action@v2
uses: eskatos/gradle-command-action@v1
with:
gradle-version: '7.4'
gradle-version: 6.8
arguments: coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
- name: Cache SonarCloud packages
uses: actions/cache@v1
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Test
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: test
- name: Build and analyze
uses: gradle/gradle-build-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
gradle-version: '7.4'
arguments: sonarqube --info
run: ./gradlew build sonarqube --info
lint:
needs: build
@@ -154,7 +128,7 @@ jobs:
name: Build
path: build
- name: Lint
uses: gradle/gradle-build-action@v2
uses: eskatos/gradle-command-action@v1
with:
gradle-version: '7.4'
gradle-version: 6.8
arguments: ktlintCheck

View File

@@ -8,7 +8,7 @@
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<SqlCodeStyleSettings version="6">
<SqlCodeStyleSettings version="5">
<option name="KEYWORD_CASE" value="1" />
<option name="IDENTIFIER_CASE" value="1" />
<option name="TYPE_CASE" value="4" />
@@ -56,13 +56,21 @@
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="5" />
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>

View File

@@ -25,9 +25,12 @@
<option value="openapi" />
<option value="rabbitmq" />
<option value="redis" />
<option value="sonarqube" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings>
</deployment>
<method v="2" />

View File

@@ -4,7 +4,7 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt" />
<option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck" />
<option name="taskDescriptions">
<list />
</option>

View File

@@ -1,23 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Create lock dependencies" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--write-locks" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="dependencies" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -24,9 +24,13 @@
<option value="rabbitmq" />
<option value="redis" />
<option value="openapi" />
<option value="sonarqube" />
<option value="sonarqube_db" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings>
</deployment>
<method v="2" />

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube (Send without run test)" type="GradleRunConfiguration" factoryName="Gradle">
<configuration default="false" name="Sonarqube without test" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />

View File

@@ -1,23 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test With Dependencies" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="testWithDependencies" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,23 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Update Dependency" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--update-locks *:*" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="classes" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,5 +1,7 @@
# DC Project
[![CodeFactor](https://www.codefactor.io/repository/github/flecomte/dc-project/badge?s=869dc426625a253a07bea95f9380e23fdb048b94)](https://www.codefactor.io/repository/github/flecomte/dc-project)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/0ec4fe63370148ca956974f90f8d55be)](https://www.codacy.com/gh/flecomte/dc-project/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=flecomte/dc-project&amp;utm_campaign=Badge_Grade)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=dc-project)
[![Tests](https://github.com/flecomte/dc-project/actions/workflows/tests.yml/badge.svg)](https://github.com/flecomte/dc-project/actions/workflows/tests.yml)

View File

@@ -9,12 +9,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.owasp.dependencycheck.reporting.ReportGenerator
import org.slf4j.LoggerFactory
val ktorVersion = "1.5.4"
val kotlinVersion = "1.5.31"
val coroutinesVersion = "1.5.2"
val ktorVersion = "1.5.0"
val kotlinVersion = "1.4.30"
val coroutinesVersion = "1.4.3"
val logbackVersion = "1.2.3"
val koinVersion = "3.1.5"
val jacksonVersion = "2.13.1"
val koinVersion = "2.0.1"
val jacksonVersion = "2.12.1"
group = "com.github.flecomte"
version = versioning.info.run {
@@ -28,24 +28,20 @@ version = versioning.info.run {
plugins {
jacoco
application
`maven-publish`
maven
kotlin("jvm") version "1.5.31"
kotlin("plugin.serialization") version "1.5.31"
id("maven-publish")
kotlin("jvm") version "1.4.30"
kotlin("plugin.serialization") version "1.4.30"
id("com.github.johnrengelman.shadow") version "7.1.2"
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
id("org.owasp.dependencycheck") version "6.1.5"
id("org.sonarqube") version "3.3"
id("net.nemerosa.versioning") version "2.15.1"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.avast.gradle.docker-compose") version "0.15.1"
id("com.github.kt3k.coveralls") version "2.12.0"
}
dependencyLocking {
lockAllConfigurations()
// lockMode.set(LockMode.STRICT)
id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
id("org.owasp.dependencycheck") version "6.1.1"
id("org.sonarqube") version "3.1.1"
id("net.nemerosa.versioning") version "2.14.0"
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
id("com.avast.gradle.docker-compose") version "0.14.0"
id("com.github.kt3k.coveralls") version "2.8.4"
}
application {
@@ -60,7 +56,7 @@ buildscript {
maven { url = uri("https://jitpack.io") }
}
dependencies {
classpath("com.typesafe:config:1.4.2")
classpath("com.typesafe:config:1.4.1")
classpath("com.github.flecomte:postgres-json:2.1.2")
}
}
@@ -98,7 +94,7 @@ val migration by tasks.registering {
}
val migrationTest by tasks.registering {
group = "tests"
group = "verification"
dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown"))
doLast {
@@ -122,9 +118,11 @@ val migrationTest by tasks.registering {
}
val testSql by tasks.registering {
group = "tests"
group = "verification"
dependsOn(tasks.named("processResources"))
dependsOn(tasks.named("processTestResources"))
dependsOn(tasks.named("testSqlComposeUp"))
finalizedBy(tasks.named("testSqlComposeDown"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
@@ -169,7 +167,6 @@ tasks.withType<Jar> {
)
)
}
isZip64 = true
}
tasks.withType<KotlinCompile> {
@@ -183,10 +180,11 @@ tasks.withType<KotlinCompile> {
tasks.named<ShadowJar>("shadowJar") {
mergeServiceFiles("META-INF/services")
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}")
isZip64 = true
}
tasks.sonarqube.configure {
dependsOn(tasks.test)
dependsOn(tasks.detekt)
dependsOn(tasks.jacocoTestReport)
}
@@ -200,6 +198,7 @@ tasks.test {
useJUnit()
useJUnitPlatform()
systemProperty("junit.jupiter.execution.parallel.enabled", true)
dependsOn(testSql)
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
}
@@ -207,6 +206,13 @@ coveralls {
sourceDirs.add("src/main/kotlin")
}
tasks.register("testAll") {
group = "verification"
dependsOn(testSql)
dependsOn(tasks.test)
dependsOn(tasks.ktlintCheck)
}
apply(plugin = "docker-compose")
dockerCompose {
projectName = "dc-project"
@@ -222,12 +228,14 @@ dockerCompose {
useComposeFiles = listOf("docker-compose-test.yml")
startedServices = listOf("db", "elasticsearch")
stopContainers = false
isRequiredBy(project.tasks.named("testSql"))
}
createNested("test").apply {
projectName = "dc-project_test"
useComposeFiles = listOf("docker-compose-test.yml")
stopContainers = false
isRequiredBy(project.tasks.test)
}
}
@@ -258,7 +266,7 @@ publishing {
}
jacoco {
toolVersion = "0.8.7"
toolVersion = "0.8.6"
applyTo(tasks.run.get())
}
@@ -312,75 +320,6 @@ tasks.named("testComposeUp").configure {
}
}
tasks.register("testWithDependencies", Test::class) {
group = "tests"
dependsOn(tasks.named("testComposeUp"))
dependsOn(tasks.ktlintCheck)
dependsOn(testSql)
dependsOn(tasks.jacocoTestReport)
finalizedBy(tasks.sonarqube) // report is always generated after tests run
}
tasks.register("testArticles", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("article")
}
}
tasks.register("testCitizens", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("citizen")
}
}
tasks.register("testComments", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("comment")
}
}
tasks.register("testConstitutions", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("constitution")
}
}
tasks.register("testFollows", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("follow")
}
}
tasks.register("testNotifications", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("notification")
}
}
tasks.register("testOpinions", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("opinion")
}
}
tasks.register("testVotes", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("vote")
}
}
tasks.register("testWorkgroups", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("workgroup")
}
}
tasks.register("testViews", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("view")
}
}
dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
}
@@ -388,6 +327,7 @@ dependencyCheck {
repositories {
mavenLocal()
jcenter()
maven { url = uri("https://kotlin.bintray.com/ktor") }
maven { url = uri("https://jitpack.io") }
}
@@ -405,7 +345,7 @@ dependencies {
implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktorVersion")
implementation("io.ktor:ktor-websockets:$ktorVersion")
implementation("io.insert-koin:koin-ktor:$koinVersion")
implementation("org.koin:koin-ktor:$koinVersion")
implementation("io.ktor:ktor-jackson:$ktorVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion")
@@ -419,18 +359,17 @@ dependencies {
implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
implementation("com.jayway.jsonpath:json-path:2.5.0")
implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.0")
implementation("io.konform:konform:0.3.0")
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion")
testImplementation("io.insert-koin:koin-test:$koinVersion")
testImplementation("org.koin:koin-test:$koinVersion")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
testImplementation("org.amshove.kluent:kluent:1.68")
testImplementation("io.mockk:mockk:1.12.2")
testImplementation("io.mockk:mockk-agent-api:1.12.2")
testImplementation("io.mockk:mockk-agent-jvm:1.12.2")
testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
testImplementation("org.amshove.kluent:kluent:1.61")
testImplementation("io.mockk:mockk-agent-api:1.10.6")
testImplementation("io.mockk:mockk-agent-jvm:1.10.6")
testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
testImplementation("com.thedeanda:lorem:2.1")
testImplementation("org.openapi4j:openapi-operation-validator:1.0.6")

30
doc/CreateAction.md Normal file
View File

@@ -0,0 +1,30 @@
Create Action
============
* [ ] Create [OpenApi](../src/main/resources/openapi.yaml) documentation
* [ ] Create route
* [ ] Create request with [Location](https://ktor.io/docs/features-locations.html)
* [ ] Create Validation of request with [Konform](https://www.konform.io)
* [ ] Test validation
* [ ] [Check auth](../src/main/kotlin/fr/dcproject/component/auth/CitizenContext.kt) on protected route
* [ ] [Create test for auth](../src/test/kotlin/integration/steps/given/Auth.kt)
* [ ] Return must not be an Entity
* [ ] Tests request:
* [ ] Route with these params
* [ ] Body of the request
* [ ] Success
* [ ] BadRequest
* [ ] Body and request params must [match with the openapi schema](../src/test/kotlin/integration/steps/then/schema.kt)
* [ ] Create [AccessControl](../src/main/kotlin/fr/dcproject/common/security/AccessControlModule.kt)
* [ ] Test [AccessControl](../src/test/kotlin/integration/steps/given/Auth.kt)
* [ ] Create Entity
* [ ] Create Repository
* [ ] Create SQL function in file
* [ ] Create Tests SQL
* [ ] Tests
* [ ] Test BadRequest

View File

@@ -1,4 +1,4 @@
version: '3.3'
version: '3.8'
services:
rabbitmq:
container_name: ${APP_NAME}_rabbitmq_test

View File

@@ -1,275 +0,0 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
ch.qos.logback:logback-classic:1.2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
ch.qos.logback:logback-core:1.2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.auth0:java-jwt:3.12.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.auth0:jwks-rsa:0.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.avast.gradle:gradle-docker-compose-plugin:0.14.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.beust:jcommander:1.81=detekt
com.fasterxml.jackson.core:jackson-annotations:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-joda:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.github.flecomte:postgres-json:2.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.github.jasync-sql:jasync-common:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-common:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.jasync-sql:jasync-pool:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-pool:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.jasync-sql:jasync-postgresql:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-postgresql:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.shyiko.klob:klob:0.2.1=ktlint
com.google.code.findbugs:jsr305:3.0.2=runtimeClasspath,testRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.2.0=runtimeClasspath,testRuntimeClasspath
com.google.guava:failureaccess:1.0.1=runtimeClasspath,testRuntimeClasspath
com.google.guava:guava:27.1-jre=runtimeClasspath,testRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=runtimeClasspath,testRuntimeClasspath
com.google.j2objc:j2objc-annotations:1.1=runtimeClasspath,testRuntimeClasspath
com.googlecode.json-simple:json-simple:1.1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.jayway.jsonpath:json-path:2.5.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.scram:client:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.scram:common:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.stringprep:saslprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.stringprep:stringprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.pinterest.ktlint:ktlint-core:0.42.1=ktlint,ktlintBaselineReporter
com.pinterest.ktlint:ktlint-reporter-baseline:0.42.1=ktlint,ktlintBaselineReporter
com.pinterest.ktlint:ktlint-reporter-checkstyle:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-html:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-json:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-plain:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-sarif:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-experimental:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-standard:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-test:0.42.1=ktlint
com.pinterest:ktlint:0.42.1=ktlint
com.rabbitmq:amqp-client:5.10.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.sendgrid:java-http-client:4.3.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.sendgrid:sendgrid-java:4.7.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.thedeanda:lorem:2.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.typesafe:config:1.3.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
commons-codec:commons-codec:1.11=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
commons-codec:commons-codec:1.14=runtimeClasspath,testRuntimeClasspath
commons-io:commons-io:2.6=runtimeClasspath,testRuntimeClasspath
commons-logging:commons-logging:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
info.picocli:picocli:3.9.6=ktlint
io.github.detekt.sarif4k:sarif4k:0.0.1=detekt,ktlint
io.github.microutils:kotlin-logging:1.7.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.gitlab.arturbosch.detekt:detekt-api:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-cli:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-core:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-metrics:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-parser:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-psi-utils:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-html:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-sarif:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-txt:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-xml:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-complexity:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-coroutines:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-documentation:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-empty:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-errorprone:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-exceptions:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-naming:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-performance:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-style:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-tooling:1.19.0=detekt
io.insert-koin:koin-core-jvm:3.1.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.insert-koin:koin-core:3.1.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.insert-koin:koin-ktor:3.1.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.insert-koin:koin-test-jvm:3.1.5=testCompileClasspath,testRuntimeClasspath
io.insert-koin:koin-test:3.1.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.konform:konform-jvm:0.3.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.konform:konform:0.3.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth-jwt-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-auth-jwt:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth-kotlinMultiplatform:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-cio-jvm:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-cio:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-core-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-core:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-jetty-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-jetty:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-mock-jvm:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-mock:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-http-cio-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-http-cio:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-http-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-http:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-io-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-io:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-jackson-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-jackson:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-locations-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-locations:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-network-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-certificates-kotlinMultiplatform:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-certificates:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-jvm:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-core-kotlinMultiplatform:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-core:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-host-common-kotlinMultiplatform:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-host-common:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-jetty-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-server-jetty:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-servlet-kotlinMultiplatform:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-servlet:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-test-host-kotlinMultiplatform:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-test-host:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-tests-kotlinMultiplatform:1.5.4=testImplementationDependenciesMetadata
io.ktor:ktor-server-tests:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-utils-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-utils:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-websockets-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-websockets:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.lettuce:lettuce-core:5.3.6.RELEASE=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-api:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-common:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-jvm:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-common:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-dsl-jvm:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-dsl:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-buffer:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-codec:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-common:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-handler:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-resolver:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-transport:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.projectreactor:reactor-core:3.4.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
javax.servlet:javax.servlet-api:3.1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
joda-time:joda-time:2.10.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
junit:junit:4.12=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.12.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.12.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.java.dev.jna:jna-platform:5.5.0=testRuntimeClasspath
net.java.dev.jna:jna:5.5.0=testRuntimeClasspath
net.minidev:accessors-smart:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.minidev:json-smart:2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.pearx.kasechange:kasechange-jvm:1.3.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.pearx.kasechange:kasechange-metadata:1.3.0=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.amshove.kluent:kluent-common:1.68=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.amshove.kluent:kluent:1.68=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpasyncclient:4.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpclient:4.5.12=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpcore-nio:4.4.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpcore:4.4.13=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata
org.bouncycastle:bcprov-jdk15on:1.67=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.checkerframework:checker-qual:2.5.2=runtimeClasspath,testRuntimeClasspath
org.codehaus.mojo:animal-sniffer-annotations:1.17=runtimeClasspath,testRuntimeClasspath
org.ec4j.core:ec4j-core:0.3.0=ktlint,ktlintBaselineReporter
org.eclipse.jetty.http2:http2-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-common:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-hpack:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-http-client-transport:9.4.31.v20200723=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-java-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-java-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-openjdk8-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-openjdk8-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-client:9.4.31.v20200723=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-continuation:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-http:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-io:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-servlets:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.elasticsearch.client:elasticsearch-rest-client:6.7.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.fusesource.jansi:jansi:2.3.4=runtimeClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath
org.jacoco:org.jacoco.agent:0.8.7=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.7=jacocoAnt
org.jacoco:org.jacoco.core:0.8.7=jacocoAnt
org.jacoco:org.jacoco.report:0.8.7=jacocoAnt
org.jetbrains.intellij.deps:trove4j:1.0.20181211=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.5.31=kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-native-utils:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-project-model:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-reflect:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-reflect:1.5.30=compileClasspath,implementationDependenciesMetadata,runtimeClasspath
org.jetbrains.kotlin:kotlin-reflect:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-script-runtime:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-script-runtime:1.5.31=detekt,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-scripting-common:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-jvm:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-serialization:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib:1.5.31=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-annotations-common:1.5.31=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-common:1.5.31=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-junit:1.5.31=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test:1.5.31=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-util-io:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.2=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-metadata:1.4.2=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.5.2=testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.1-native-mt=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.3=detekt
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-core-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-json-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0=detekt,ktlint
org.jetbrains:annotations:13.0=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.joda:joda-convert:1.8.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.8.2=testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.8.2=testRuntimeClasspath
org.junit:junit-bom:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.objenesis:objenesis:3.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-core:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-operation-validator:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-parser:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-schema-validator:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.1=jacocoAnt
org.ow2.asm:asm-commons:9.1=jacocoAnt
org.ow2.asm:asm-tree:9.1=jacocoAnt
org.ow2.asm:asm:5.0.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.ow2.asm:asm:9.1=jacocoAnt
org.reactivestreams:reactive-streams:1.0.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.slf4j:slf4j-api:1.7.30=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.yaml:snakeyaml:1.27=runtimeClasspath
org.yaml:snakeyaml:1.28=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.yaml:snakeyaml:1.29=detekt
empty=annotationProcessor,apiDependenciesMetadata,compileOnly,compileOnlyDependenciesMetadata,detektPlugins,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,ktlintReporter,ktlintRuleset,runtimeOnlyDependenciesMetadata,shadow,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntimeOnlyDependenciesMetadata

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -9,7 +9,6 @@ import com.fasterxml.jackson.datatype.joda.JodaModule
import fr.dcproject.application.Env.PROD
import fr.dcproject.application.Env.TEST
import fr.dcproject.application.http.statusPagesInstallation
import fr.dcproject.common.utils.onApplicationStopped
import fr.dcproject.component.article.articleKoinModule
import fr.dcproject.component.article.routes.installArticleRoutes
import fr.dcproject.component.auth.authKoinModule
@@ -26,10 +25,8 @@ import fr.dcproject.component.constitution.routes.installConstitutionRoutes
import fr.dcproject.component.doc.routes.installDocRoutes
import fr.dcproject.component.follow.followKoinModule
import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes
import fr.dcproject.component.follow.routes.citizen.installFollowCitizenRoutes
import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes
import fr.dcproject.component.notification.email.NotificationEmailConsumer
import fr.dcproject.component.notification.push.NotificationPushConsumer
import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.routes.installNotificationsRoutes
import fr.dcproject.component.opinion.opinionKoinModule
import fr.dcproject.component.opinion.routes.installOpinionRoutes
@@ -40,6 +37,7 @@ import fr.dcproject.component.workgroup.routes.installWorkgroupRoutes
import fr.dcproject.component.workgroup.workgroupKoinModule
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.client.HttpClient
@@ -59,6 +57,7 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations
import io.ktor.routing.Routing
import io.ktor.server.jetty.EngineMain
import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog
@@ -73,6 +72,7 @@ fun main(args: Array<String>): Unit = EngineMain.main(args)
enum class Env { PROD, TEST }
@ExperimentalCoroutinesApi
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module(env: Env = PROD) {
@@ -117,14 +117,11 @@ fun Application.module(env: Env = PROD) {
masking = false
}
get<NotificationEmailConsumer>().run {
get<NotificationConsumer>().run {
start()
onApplicationStopped { close() }
environment.monitor.subscribe(ApplicationStopped) {
close()
}
get<NotificationPushConsumer>().run {
start()
onApplicationStopped { close() }
}
install(Authentication, jwtInstallation(get(), get()))
@@ -157,7 +154,6 @@ fun Application.module(env: Env = PROD) {
installCommentRoutes()
installFollowArticleRoutes()
installFollowConstitutionRoutes()
installFollowCitizenRoutes()
installWorkgroupRoutes()
installOpinionRoutes()
installVoteRoutes()

View File

@@ -1,32 +1,23 @@
package fr.dcproject.application
import fr.dcproject.application.http.BadRequestException
import fr.dcproject.application.http.HttpErrorBadRequest
import fr.dcproject.application.http.HttpErrorBadRequest.InvalidParam
import io.ktor.features.DataConversion
import io.ktor.http.HttpStatusCode
import io.ktor.util.KtorExperimentalAPI
import org.koin.core.context.GlobalContext
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import java.util.UUID
private typealias ConverterDeclaration = DataConversion.Configuration.() -> Unit
private inline fun <reified T> DataConversion.Configuration.get(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): T = GlobalContext.get().koin.rootScope.get(qualifier, parameters)
@KtorExperimentalAPI
val converters: ConverterDeclaration = {
convert<UUID> {
decode { values, _ ->
try {
values.singleOrNull()?.let { UUID.fromString(it) }
} catch (e: Throwable) {
throw BadRequestException(
HttpErrorBadRequest(
HttpStatusCode.BadRequest,
invalidParams = listOf(
InvalidParam(
"ID",
"must be UUID"
)
)
)
)
}
}
encode { value ->

View File

@@ -10,20 +10,21 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.common.email.Mailer
import fr.dcproject.component.auth.jwt.JwtConfig
import fr.dcproject.component.notification.NotificationPublisherAsync
import fr.dcproject.component.notification.email.NotificationEmailConsumer
import fr.dcproject.component.notification.email.NotificationEmailSender
import fr.dcproject.component.notification.push.NotificationPushConsumer
import fr.dcproject.component.notification.push.NotificationPushListener
import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.NotificationEmailSender
import fr.dcproject.component.notification.NotificationsPush
import fr.dcproject.component.notification.Publisher
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient
import org.koin.core.qualifier.named
import org.koin.dsl.module
@KtorExperimentalAPI
val KoinModule = module {
// JWT
single {
@@ -64,15 +65,11 @@ val KoinModule = module {
}
}
single { NotificationPushListener.Builder(get()) }
single { NotificationsPush.Builder(get()) }
single {
val config: Configuration = get()
NotificationEmailConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
}
single {
val config: Configuration = get()
NotificationPushConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
}
// RabbitMQ
@@ -117,7 +114,7 @@ val KoinModule = module {
single {
val config: Configuration = get()
NotificationPublisherAsync(factory = get(), exchangeName = config.exchangeNotificationName)
Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
}
single {

View File

@@ -1,35 +0,0 @@
package fr.dcproject.application.http
import fr.dcproject.application.http.HttpErrorBadRequest.InvalidParam
import io.konform.validation.ValidationResult
import io.ktor.http.HttpStatusCode
class BadRequestException(val httpError: HttpErrorBadRequest) : Exception()
class HttpErrorBadRequest(
statusCode: HttpStatusCode,
val title: String = statusCode.description,
val invalidParams: List<InvalidParam>,
) {
val statusCode: Int = statusCode.value
data class InvalidParam(
val name: String,
val reason: String
)
}
fun ValidationResult<*>.toOutput() = HttpErrorBadRequest(
HttpStatusCode.BadRequest,
invalidParams = this.errors.map {
InvalidParam(
it.dataPath,
it.message
)
}
)
fun ValidationResult<*>.badRequestIfNotValid() {
if (errors.size > 0) {
throw BadRequestException(toOutput())
}
}

View File

@@ -6,7 +6,6 @@ import fr.dcproject.component.auth.ForbiddenException
import fr.dcproject.component.auth.user
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.features.ParameterConversionException
import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
@@ -14,10 +13,18 @@ import java.util.concurrent.CompletionException
class HttpError(
statusCode: HttpStatusCode,
cause: Throwable? = null,
val cause: Throwable? = null,
val type: String? = null,
val title: String = cause?.message ?: statusCode.description,
val detail: String? = null,
val invalidParams: List<InvalidParam>? = null,
val stackTrace: String? = cause?.stackTraceToString()
) {
val statusCode: Int = statusCode.value
data class InvalidParam(
val name: String,
val reason: String
)
}
fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = {
@@ -72,15 +79,4 @@ fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = {
call.respond(HttpStatusCode.Forbidden, it)
}
}
exception<BadRequestException> { e ->
call.respond(HttpStatusCode.BadRequest, e.httpError)
}
exception<ParameterConversionException> { e ->
val parent = e.cause
if (parent is BadRequestException) {
call.respond(HttpStatusCode.BadRequest, parent.httpError)
} else {
throw e
}
}
}

View File

@@ -9,9 +9,6 @@ import java.io.IOException
class Mailer(
private val key: String
) {
/**
* Send email via Sendgrid
*/
fun sendEmail(action: () -> Mail): Boolean {
val mail = action()

View File

@@ -2,7 +2,6 @@ package fr.dcproject.common.entity
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.opinion.database.OpinionRef
@@ -35,8 +34,7 @@ interface TargetI : EntityI {
Article("article"),
Constitution("constitution"),
Comment("comment"),
Opinion("opinion"),
Citizen("citizen"),
Opinion("opinion")
}
companion object {
@@ -46,7 +44,6 @@ interface TargetI : EntityI {
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference
t.isSubclassOf(CitizenRef::class) -> TargetName.Citizen.targetReference
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
}
}

View File

@@ -6,6 +6,9 @@ interface PaginatedRequestI {
}
open class PaginatedRequest(
override val page: Int = 1,
override val limit: Int = 50
) : PaginatedRequestI
page: Int = 1,
limit: Int = 50
) : PaginatedRequestI {
override val page: Int = if (page < 1) 1 else page
override val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}

View File

@@ -4,6 +4,7 @@ import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.PathNotFoundException
import org.apache.http.util.EntityUtils
import org.elasticsearch.client.Response
import org.slf4j.LoggerFactory
fun Response.contentToString(): String {
return EntityUtils.toString(this.entity)
@@ -21,6 +22,8 @@ fun String.getJsonField(jsonPath: String): Int? {
return try {
JsonPath.read(this, jsonPath)
} catch (e: PathNotFoundException) {
LoggerFactory.getLogger("fr.dcproject.utils.getJsonField")
.warn("No value for Json path ${JsonPath.compile(jsonPath).path}")
null
}
}

View File

@@ -1,10 +0,0 @@
package fr.dcproject.common.utils
import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
fun Application.onApplicationStopped(callback: Application.() -> Unit) {
environment.monitor.subscribe(ApplicationStopped) {
callback()
}
}

View File

@@ -1,30 +0,0 @@
package fr.dcproject.common.utils
import com.rabbitmq.client.AMQP
import com.rabbitmq.client.Channel
import com.rabbitmq.client.Consumer
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.Envelope
import kotlinx.coroutines.runBlocking
import java.io.IOException
fun Channel.consumeQueue(queueName: String, callback: DefaultConsumer.(ByteArray) -> Unit) {
val consumer: Consumer = object : DefaultConsumer(this) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: AMQP.BasicProperties,
body: ByteArray
) = runBlocking {
try {
callback(body)
basicAck(envelope.deliveryTag, false)
} catch (e: Throwable) {
basicNack(envelope.deliveryTag, false, true)
}
}
}
/* Launch Consumer */
basicConsume(queueName, false, consumer)
}

View File

@@ -1,41 +0,0 @@
package fr.dcproject.common.utils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
@ExperimentalTime
fun <T> retry(numOfRetries: Int, duration: Duration = Duration.ZERO, block: (RetryContext) -> T): T {
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.utils.retry")
var throwable: Throwable? = null
for (attempt in 1..numOfRetries) {
val context = RetryContext()
try {
val output = block(context)
if (context.hasStop()) {
break
}
return output
} catch (e: Throwable) {
throwable = e
logger.debug("Failed attempt $attempt / $numOfRetries. Wait ${duration.inSeconds} seconds")
Thread.sleep(duration.inMilliseconds.toLong())
} finally {
if (context.hasStop()) {
break
}
}
}
throw throwable!!
}
class RetryContext() {
var stoped = false
fun stop() {
stoped = true
}
fun hasStop(): Boolean = stoped
}

View File

@@ -1,6 +0,0 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
import io.konform.validation.jsonschema.pattern
fun ValidationBuilder<String>.email() = pattern(""".+@.+\..+""")

View File

@@ -1,22 +0,0 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
fun ValidationBuilder<String>.passwordScore(minScore: Int) =
addConstraint("is not enough strong. Use Upper case, Lower case and special characters or juste use more characters.") { value ->
value.passwordScore() >= minScore
}
fun String.passwordScore(): Int {
var score: Int = length
val alphaNum = ('a'..'z').toList() + ('A'..'Z').toList() + ('0'..'9').toList()
val specialCount = length - toList().intersect(alphaNum).size
score += specialCount.let { if (it > 3) 3 else it }
val hasAlphaLower = toList().intersect(('a'..'z').toList()).size.let { if (it > 2) 2 else it }
val hasAlphaUpper = toList().intersect(('A'..'Z').toList()).size.let { if (it > 2) 2 else it }
val hasNum = toList().intersect(('0'..'9').toList()).size.let { if (it > 2) 2 else it }
score += (hasAlphaLower + hasAlphaUpper + hasNum - 2) * 2
return score
}

View File

@@ -1,15 +0,0 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
import java.net.MalformedURLException
import java.net.URL
fun ValidationBuilder<String>.isUrl() =
addConstraint("is not url") {
try {
URL(it)
true
} catch (e: MalformedURLException) {
false
}
}

View File

@@ -1,14 +0,0 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
import java.util.UUID
fun ValidationBuilder<String>.isUuid() =
addConstraint("must be UUID") {
try {
UUID.fromString(it)
true
} catch (exception: IllegalArgumentException) {
false
}
}

View File

@@ -37,7 +37,7 @@ class ArticleAccessControl(private val articleRepo: ArticleRepository) : AccessC
if (subject.createdBy.id == citizen.id) {
/* The creator must be the same of the creator of preview version of article */
val lastVersionId = articleRepo
.findSiblingVersions(1, 1, subject)
.findVersionsByVersionId(1, 1, subject.versionId)
.result
.firstOrNull()?.createdBy?.id

View File

@@ -48,7 +48,7 @@ data class ArticleForView(
val lastVersion: Boolean = false
}
sealed interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, VersionableId, TargetI, CreatedBy<C> {
interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, VersionableId, TargetI, CreatedBy<C> {
val anonymous: Boolean
val content: String
val description: String
@@ -56,13 +56,13 @@ sealed interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI
val workgroup: WorkgroupRef?
}
data class ArticleForUpdate(
class ArticleForUpdate(
override val id: UUID = UUID.randomUUID(),
override val title: String,
override val anonymous: Boolean = true,
override val content: String,
override val description: String,
val tags: Set<String> = emptySet(),
tags: List<String> = emptyList(),
override val draft: Boolean = false,
override val createdBy: CitizenRef,
override val workgroup: WorkgroupRef? = null,
@@ -71,10 +71,12 @@ data class ArticleForUpdate(
) : ArticleRef(id),
ArticleForUpdateI<CitizenRef>,
ArticleAuthI<CitizenRef>,
VersionableId
VersionableId {
val tags: List<String> = tags.distinct()
}
data class ArticleForListing(
override val id: UUID = UUID.randomUUID(),
class ArticleForListing(
id: UUID? = null,
override val title: String,
override val createdBy: CitizenCreator,
override val workgroup: WorkgroupCart? = null,
@@ -85,10 +87,9 @@ data class ArticleForListing(
ArticleRef(id),
ArticleAuthI<CitizenCartI>,
Votable by VotableImp(),
CreatedAt by CreatedAt.Imp(),
CreatedBy<CitizenCartI>
sealed interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {
interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {
val workgroup: WorkgroupCartI?
}
@@ -96,13 +97,13 @@ open class ArticleRef(
id: UUID? = null
) : ArticleI, TargetRef(id)
sealed interface ArticleI : EntityI, TargetI
interface ArticleI : EntityI, TargetI
sealed interface ArticleWithTitleI : ArticleI {
interface ArticleWithTitleI : ArticleI {
val title: String
}
sealed interface ArticleAuthI<U : CitizenI> :
interface ArticleAuthI<U : CitizenI> :
ArticleI,
CreatedBy<U>,
DeletedAt {

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.article.database
import fr.dcproject.common.entity.VersionableId
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter
@@ -20,10 +19,10 @@ class ArticleRepository(override var requester: Requester) : RepositoryI {
.select(page, limit, "id" to id)
}
fun <A> findSiblingVersions(page: Int = 1, limit: Int = 50, article: A): Paginated<ArticleForListing> where A : VersionableId, A : ArticleI {
fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForListing> {
return requester
.getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to article.versionId)
.select(page, limit, "version_id" to versionId)
}
fun find(

View File

@@ -1,71 +1,44 @@
package fr.dcproject.component.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.toUUID
import fr.dcproject.common.validation.isUuid
import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.database.ArticleForListing
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object FindArticleVersions {
@Location("/articles/{article}/versions")
class ArticleVersionsRequest(
val article: String,
article: UUID,
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) {
fun validate() = Validation<ArticleVersionsRequest> {
ArticleVersionsRequest::page {
minimum(1)
maximum(100)
}
ArticleVersionsRequest::limit {
minimum(1)
maximum(50)
}
ArticleVersionsRequest::sort ifPresent {
enum(
"title",
"createdAt",
"vote",
"popularity",
)
}
ArticleVersionsRequest::article {
isUuid()
}
}.validate(this)
) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
val article = ArticleRef(article)
}
private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) =
findVersionsById(request.page, request.limit, request.article.toUUID())
findVersionsById(request.page, request.limit, request.article.id)
fun Route.findArticleVersions(repo: ArticleRepository, ac: ArticleAccessControl) {
get<ArticleVersionsRequest> {
it.validate().badRequestIfNotValid()
repo.findVersions(it)
.apply { ac.canView(result, citizenOrNull).assert() }
.apply { ac.assert { canView(result, citizenOrNull) } }
.run {
call.respond(
toOutput { a: ArticleForListing ->

View File

@@ -1,9 +1,7 @@
package fr.dcproject.component.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.common.validation.isUuid
import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.database.ArticleForListing
import fr.dcproject.component.article.database.ArticleRepository
@@ -12,10 +10,6 @@ import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
@@ -34,31 +28,7 @@ object FindArticles {
val search: String? = null,
val createdBy: String? = null,
val workgroup: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) {
fun validate() = Validation<ArticlesRequest> {
ArticlesRequest::page {
minimum(1)
}
ArticlesRequest::limit {
minimum(1)
maximum(50)
}
ArticlesRequest::sort ifPresent {
enum(
"title",
"createdAt",
"vote",
"popularity",
)
}
ArticlesRequest::createdBy ifPresent {
isUuid()
}
ArticlesRequest::workgroup ifPresent {
isUuid()
}
}.validate(this)
}
) : PaginatedRequestI by PaginatedRequest(page, limit)
private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<ArticleForListing> {
return find(
@@ -73,17 +43,14 @@ object FindArticles {
fun Route.findArticles(repo: ArticleRepository, ac: ArticleAccessControl) {
get<ArticlesRequest> {
it.validate().badRequestIfNotValid()
repo.findArticles(it)
.apply { ac.canView(result, citizenOrNull).assert() }
.apply { ac.assert { canView(result, citizenOrNull) } }
.let {
call.respond(
it.toOutput {
object {
val id = it.id
val title = it.title
val createdAt = it.createdAt
val createdBy: Any = it.createdBy.toOutput()
val workgroup = it.workgroup?.let {
object {

View File

@@ -27,7 +27,7 @@ object GetOneArticle {
fun Route.getOneArticle(viewRepository: ArticleViewRepository<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
get<ArticleRequest> {
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
ac.canView(article, citizenOrNull).assert()
ac.assert { canView(article, citizenOrNull) }
call.respond(
article.let { a ->

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.ArticleAccessControl
@@ -10,14 +9,9 @@ import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage
import fr.dcproject.component.notification.NotificationPublisherAsync
import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.workgroup.database.WorkgroupRef
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxItems
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minItems
import io.konform.validation.jsonschema.minLength
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -37,35 +31,15 @@ object UpsertArticle {
val anonymous: Boolean = true,
val content: String,
val description: String,
val tags: Set<String> = emptySet(),
val tags: List<String> = emptyList(),
val draft: Boolean = false,
val versionId: UUID,
val workgroup: WorkgroupRef? = null,
) {
fun validate() = Validation<Input> {
Input::title {
minLength(5)
maxLength(80)
}
Input::content {
minLength(50)
maxLength(6000)
}
Input::description {
minLength(50)
maxLength(6000)
}
Input::tags {
minItems(0)
maxItems(15)
}
}.validate(this)
}
)
}
fun Route.upsertArticle(repo: ArticleRepository, notificationPublisher: NotificationPublisherAsync, ac: ArticleAccessControl) {
fun Route.upsertArticle(repo: ArticleRepository, publisher: Publisher, ac: ArticleAccessControl) {
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest<Input>().run {
validate().badRequestIfNotValid()
ArticleForUpdate(
id = id ?: UUID.randomUUID(),
title = title,
@@ -83,7 +57,7 @@ object UpsertArticle {
post<UpsertArticleRequest> {
mustBeAuth()
val article = call.convertRequestToEntity()
ac.canUpsert(article, citizenOrNull).assert()
ac.assert { canUpsert(article, citizenOrNull) }
repo.upsert(article)?.let { a ->
call.respond(
object {
@@ -92,7 +66,7 @@ object UpsertArticle {
val versionNumber = a.versionNumber
}
)
notificationPublisher.publishAsync(ArticleUpdateNotificationMessage(a))
publisher.publish(ArticleUpdateNotification(a))
} ?: error("Article not updated")
}
}

View File

@@ -17,13 +17,13 @@ private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
val ApplicationCall.citizen: CitizenEntity
get() = attributes.computeIfAbsent(citizenAttributeKey) {
val user = authentication.principal<UserI>() ?: throw ForbiddenException("No User Connected")
GlobalContext.get().get<CitizenRepository>().findByUser(user)
GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"")
}
val ApplicationCall.citizenOrNull: CitizenEntity?
get() = authentication.principal<UserI>()?.let {
GlobalContext.get().get<CitizenRepository>().findByUser(it)
GlobalContext.get().koin.get<CitizenRepository>().findByUser(it)
}
val ApplicationCall.isAuth: Boolean

View File

@@ -9,45 +9,35 @@ import io.ktor.auth.Principal
import org.joda.time.DateTime
import java.util.UUID
data class UserForCreate(
override val id: UUID = UUID.randomUUID(),
override val username: String,
class UserForCreate(
id: UUID = UUID.randomUUID(),
username: String,
override val password: String,
override val blockedAt: DateTime? = null,
override val roles: Set<Roles> = emptySet()
) : UserForViewI,
UserWithPasswordI,
CreatedAt by CreatedAt.Imp(),
UpdatedAt by UpdatedAt.Imp()
blockedAt: DateTime? = null,
roles: List<Roles> = emptyList()
) : User(id, username, blockedAt, roles),
UserWithPasswordI
data class User(
override val id: UUID = UUID.randomUUID(),
override val username: String,
override val blockedAt: DateTime? = null,
override val roles: Set<Roles> = emptySet()
open class User(
id: UUID = UUID.randomUUID(),
override var username: String,
var blockedAt: DateTime? = null,
var roles: List<Roles> = emptyList()
) : UserRef(id),
UserForViewI,
UserWithUsername,
CreatedAt by CreatedAt.Imp(),
UpdatedAt by UpdatedAt.Imp()
sealed interface UserForViewI :
UserI,
UserWithUsername,
UserForAuthI,
CreatedAt,
UpdatedAt
class UserCreator(
id: UUID = UUID.randomUUID(),
override val username: String,
) : UserRef(id), UserWithUsername
sealed interface UserWithUsername : UserI {
interface UserWithUsername : UserI {
val username: String
}
sealed interface UserWithPasswordI : UserI {
interface UserWithPasswordI : UserI {
val password: String
}
@@ -61,11 +51,11 @@ open class UserRef(
id: UUID = UUID.randomUUID()
) : UserI, Entity(id)
sealed interface UserI : EntityI, Principal {
interface UserI : EntityI, Principal {
enum class Roles { ROLE_USER, ROLE_ADMIN }
}
sealed interface UserForAuthI : UserI {
val roles: Set<Roles>
val blockedAt: DateTime?
interface UserForAuthI : UserI {
var roles: List<Roles>
var blockedAt: DateTime?
}

View File

@@ -7,7 +7,7 @@ import org.koin.core.context.GlobalContext
/**
* Produce a token for this combination of User and Account
*/
fun UserI.makeToken(): String = GlobalContext.get().get<JwtConfig>().run {
fun UserI.makeToken(): String = GlobalContext.get().koin.get<JwtConfig>().run {
JWT.create()
.withSubject("Authentication")
.withIssuer(issuer)

View File

@@ -1,10 +1,7 @@
package fr.dcproject.component.auth.routes
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.common.validation.email
import fr.dcproject.common.validation.passwordScore
import fr.dcproject.component.auth.database.UserForCreate
import fr.dcproject.component.auth.database.UserI
import fr.dcproject.component.auth.jwt.makeToken
@@ -12,9 +9,6 @@ import fr.dcproject.component.auth.routes.Register.RegisterRequest.Input
import fr.dcproject.component.citizen.database.CitizenForCreate
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRepository
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.features.BadRequestException
import io.ktor.http.ContentType
@@ -49,35 +43,6 @@ object Register {
val username: String,
val password: String
)
fun validate() = Validation<Input> {
Input::name {
Name::firstName {
minLength(2)
maxLength(50)
}
Name::lastName {
minLength(2)
maxLength(50)
}
Name::civility ifPresent {
minLength(1)
maxLength(10)
}
}
Input::user {
User::username {
minLength(7)
maxLength(30)
}
User::password {
passwordScore(15)
}
}
Input::email {
email()
}
}.validate(this)
}
}
@@ -91,16 +56,13 @@ object Register {
user = UserForCreate(
username = user.username,
password = user.password,
roles = setOf(UserI.Roles.ROLE_USER)
roles = listOf(UserI.Roles.ROLE_USER)
)
)
post<RegisterRequest> {
try {
val citizen = call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
.toCitizen()
val citizen = call.receiveOrBadRequest<Input>().toCitizen()
citizenRepo.insertWithUser(citizen)?.user?.makeToken()?.let { token ->
if (call.request.accept() == ContentType.Application.Json.toString()) {
call.respond(

View File

@@ -2,9 +2,8 @@ package fr.dcproject.component.citizen.database
import fr.dcproject.common.entity.CreatedAt
import fr.dcproject.common.entity.DeletedAt
import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.EntityI
import fr.dcproject.common.entity.TargetI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserForCreate
@@ -17,19 +16,19 @@ import fr.postgresjson.entity.Serializable
import org.joda.time.DateTime
import java.util.UUID
data class CitizenForCreate(
class CitizenForCreate(
val name: Name,
val email: String,
val birthday: DateTime,
val voteAnonymous: Boolean = true,
val followAnonymous: Boolean = true,
override val user: UserForCreate,
override val id: UUID = UUID.randomUUID(),
id: UUID = UUID.randomUUID(),
) : CitizenI,
CitizenWithUserI by CitizenRefWithUser(id, user),
CitizenRefWithUser(id, user),
CreatedAt by CreatedAt.Imp()
data class Citizen(
class Citizen(
override val id: UUID = UUID.randomUUID(),
override val name: Name,
override val email: String,
@@ -37,7 +36,7 @@ data class Citizen(
override val voteAnonymous: Boolean = true,
override val followAnonymous: Boolean = true,
override val user: User,
override val deletedAt: DateTime? = null
deletedAt: DateTime? = null
) : CitizenWithEmail,
CitizenCreatorI,
CitizenWithUserI,
@@ -62,11 +61,10 @@ data class CitizenCreator(
override val user: UserCreator,
override val deletedAt: DateTime? = null
) : CitizenCreatorI,
CitizenI,
CitizenWithUserI by CitizenRefWithUser(id, user),
CitizenRefWithUser(id, user),
DeletedAt by DeletedAt.Imp(deletedAt)
sealed interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, DeletedAt {
interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, DeletedAt {
override val id: UUID
override val name: Name
override val email: String
@@ -76,8 +74,8 @@ sealed interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCa
override val deletedAt: DateTime?
}
data class CitizenCart(
override val id: UUID = UUID.randomUUID(),
class CitizenCart(
id: UUID = UUID.randomUUID(),
override val name: Name,
override val user: UserRef,
override val deletedAt: DateTime? = null,
@@ -85,22 +83,22 @@ data class CitizenCart(
CitizenCartI,
DeletedAt by DeletedAt.Imp(deletedAt)
sealed interface CitizenCartI : CitizenI, CitizenWithUserI {
interface CitizenCartI : CitizenI, CitizenWithUserI {
val name: Name
}
data class CitizenRefWithUser(
override val id: UUID = UUID.randomUUID(),
override val user: UserI
open class CitizenRefWithUser(
id: UUID = UUID.randomUUID(),
override val user: UserRef
) : CitizenWithUserI,
CitizenRef(id)
open class CitizenRef(
id: UUID = UUID.randomUUID()
) : TargetRef(id),
) : Entity(id),
CitizenI
sealed interface CitizenI : EntityI, TargetI {
interface CitizenI : EntityI {
data class Name(
override val firstName: String,
override val lastName: String,
@@ -115,10 +113,10 @@ sealed interface CitizenI : EntityI, TargetI {
}
}
sealed interface CitizenWithUserI : CitizenI {
interface CitizenWithUserI : CitizenI {
val user: UserI
}
sealed interface CitizenWithEmail : CitizenI {
interface CitizenWithEmail : CitizenI {
val email: String
}

View File

@@ -1,9 +1,7 @@
package fr.dcproject.component.citizen.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.common.validation.passwordScore
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.database.UserRepository
@@ -11,7 +9,6 @@ import fr.dcproject.component.auth.database.UserWithPassword
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.CitizenAccessControl
import fr.dcproject.component.citizen.database.CitizenRef
import io.konform.validation.Validation
import io.ktor.application.call
import io.ktor.auth.UserPasswordCredential
import io.ktor.features.BadRequestException
@@ -28,21 +25,14 @@ object ChangeMyPassword {
@Location("/citizens/{citizen}/password/change")
class ChangePasswordCitizenRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
data class Input(val oldPassword: String, val newPassword: String) {
fun validate() = Validation<Input> {
Input::newPassword {
passwordScore(15)
}
}.validate(this)
}
data class Input(val oldPassword: String, val newPassword: String)
}
fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) {
put<ChangePasswordCitizenRequest> {
mustBeAuth()
ac.assert { canChangePassword(it.citizen, citizenOrNull) }
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
.apply { validate().badRequestIfNotValid() }
ac.canChangePassword(it.citizen, citizenOrNull).assert()
userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password")
userRepository.changePassword(
UserWithPassword(

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.citizen.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
@@ -11,10 +10,6 @@ import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
@@ -32,30 +27,13 @@ object FindCitizens {
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) {
fun validate() = Validation<CitizensRequest> {
CitizensRequest::page {
minimum(1)
}
CitizensRequest::limit {
minimum(1)
maximum(50)
}
CitizensRequest::sort ifPresent {
enum(
"title",
"createdAt",
)
}
}.validate(this)
}
) : PaginatedRequestI by PaginatedRequest(page, limit)
fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) {
get<CitizensRequest> {
mustBeAuth()
it.validate().badRequestIfNotValid()
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
ac.canView(citizens.result, citizenOrNull).assert()
ac.assert { canView(citizens.result, citizenOrNull) }
call.respond(
citizens.toOutput { c: CitizenCreator ->
object {

View File

@@ -28,7 +28,7 @@ object GetCurrentCitizen {
if (currentUser === null) {
call.respond(HttpStatusCode.Unauthorized)
} else {
ac.canView(currentUser, citizenOrNull).assert()
ac.assert { canView(currentUser, citizenOrNull) }
call.respond(
object {
val id: UUID = citizen.id

View File

@@ -29,7 +29,7 @@ object GetOneCitizen {
get<CitizenRequest> {
mustBeAuth()
val citizen = citizenRepository.findById(it.citizen.id) ?: throw NotFoundException("Citizen not found ${it.citizen.id}")
ac.canView(citizen, citizenOrNull).assert()
ac.assert { canView(citizen, citizenOrNull) }
call.respond(
object {

View File

@@ -41,7 +41,7 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
target: EntityI,
page: Int,
limit: Int,
sort: String
sort: Sort
): Paginated<CommentForView<ArticleForView, CitizenCreatorI>> {
return requester
.getFunction("find_comments_by_target")
@@ -49,7 +49,18 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
page,
limit,
"target_id" to target.id,
"sort" to sort
"sort" to sort.sql
) as Paginated<CommentForView<ArticleForView, CitizenCreatorI>>
}
enum class Sort(val sql: String) {
CREATED_AT("created_at"),
VOTES("votes");
companion object {
fun fromString(string: String): Sort? {
return values().firstOrNull { it.sql == string }
}
}
}
}

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.comment.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef
@@ -12,9 +12,6 @@ import fr.dcproject.component.comment.article.routes.CreateCommentArticle.PostAr
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.toOutput
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -29,29 +26,20 @@ object CreateCommentArticle {
@Location("/articles/{article}/comments")
class PostArticleCommentRequest(article: UUID) {
val article = ArticleRef(article)
class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
class Input(val content: String)
}
fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) {
post<PostArticleCommentRequest> {
mustBeAuth()
call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
.run {
call.receiveOrBadRequest<Input>().run {
CommentForUpdate(
target = it.article,
createdBy = citizen,
content = content
)
}.let { comment ->
ac.canCreate(comment, citizenOrNull).assert()
ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
call.respond(

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.comment.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.article.database.ArticleRef
@@ -10,10 +9,6 @@ import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.toOutput
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -31,34 +26,17 @@ object GetArticleComments {
page: Int = 1,
limit: Int = 50,
val search: String? = null,
val sort: String = "createdAt"
sort: String = CommentArticleRepository.Sort.CREATED_AT.sql
) : PaginatedRequestI by PaginatedRequest(page, limit) {
val article = ArticleRef(article)
fun validate() = Validation<ArticleCommentsRequest> {
ArticleCommentsRequest::page {
minimum(1)
}
ArticleCommentsRequest::limit {
minimum(1)
maximum(50)
}
ArticleCommentsRequest::sort ifPresent {
enum(
"votes",
"createdAt",
)
}
}.validate(this)
val sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.fromString(sort) ?: CommentArticleRepository.Sort.CREATED_AT
}
fun Route.getArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) {
get<ArticleCommentsRequest> {
it.validate().badRequestIfNotValid()
val comments = repo.findByTarget(it.article, it.page, it.limit, it.sort)
if (comments.result.isNotEmpty()) {
ac.canView(comments.result, citizenOrNull).assert()
ac.assert { canView(comments.result, citizenOrNull) }
}
call.respond(
HttpStatusCode.OK,

View File

@@ -28,7 +28,7 @@ object GetCitizenArticleComments {
get<CitizenCommentArticleRequest> {
mustBeAuth()
repo.findByCitizen(it.citizen).let { comments ->
ac.canView(comments.result, citizenOrNull).assert()
ac.assert { canView(comments.result, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
comments.toOutput { comment ->

View File

@@ -5,6 +5,7 @@ import fr.dcproject.common.entity.TargetI
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.comment.article.database.CommentArticleRepository
import fr.dcproject.component.comment.generic.database.CommentForView
import fr.dcproject.component.comment.generic.database.CommentRepositoryAbs
import fr.dcproject.component.constitution.database.ConstitutionRef
@@ -40,7 +41,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
target: EntityI,
page: Int,
limit: Int,
sort: String
sort: CommentArticleRepository.Sort
): Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_target")
@@ -48,7 +49,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
page,
limit,
"target_id" to target.id,
"sort" to sort
"sort" to sort.sql
)
as Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>>
}

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.comment.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
@@ -13,9 +12,6 @@ import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.toOutput
import fr.dcproject.component.constitution.database.ConstitutionRef
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -30,30 +26,20 @@ object CreateConstitutionComment {
@Location("/constitutions/{constitution}/comments")
class CreateConstitutionCommentRequest(constitution: UUID) {
val constitution = ConstitutionRef(constitution)
class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
class Input(val content: String)
}
fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
post<CreateConstitutionCommentRequest> {
mustBeAuth()
call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
.run {
call.receiveOrBadRequest<Input>().run {
CommentForUpdate(
target = it.constitution,
createdBy = citizen,
content = content
)
}.let { comment ->
ac.canCreate(comment, citizenOrNull).assert()
ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
call.respond(

View File

@@ -28,7 +28,7 @@ object GetCitizenCommentConstitution {
get<GetCitizenCommentConstitutionRequest> {
mustBeAuth()
val comments = repo.findByCitizen(it.citizen)
ac.canView(comments.result, citizenOrNull).assert()
ac.assert { canView(comments.result, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
comments.toOutput { comment ->

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.comment.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
@@ -8,12 +7,6 @@ import fr.dcproject.component.comment.constitution.database.CommentConstitutionR
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.toOutput
import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -26,38 +19,14 @@ import java.util.UUID
@KtorExperimentalLocationsAPI
object GetConstitutionComment {
@Location("/constitutions/{constitution}/comments")
class GetConstitutionCommentRequest(
constitution: UUID,
page: Int = 1,
limit: Int = 50,
val search: String? = null,
val sort: String = "createdAt"
) : PaginatedRequestI by PaginatedRequest(page, limit) {
class GetConstitutionCommentRequest(constitution: UUID) {
val constitution = ConstitutionRef(constitution)
fun validate() = Validation<GetConstitutionCommentRequest> {
GetConstitutionCommentRequest::page {
minimum(1)
}
GetConstitutionCommentRequest::limit {
minimum(1)
maximum(50)
}
GetConstitutionCommentRequest::sort ifPresent {
enum(
"votes",
"createdAt",
)
}
}.validate(this)
}
fun Route.getConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
get<GetConstitutionCommentRequest> {
it.validate().badRequestIfNotValid()
val comments = repo.findByTarget(it.constitution)
ac.canView(comments.result, citizenOrNull).assert()
ac.assert { canView(comments.result, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
comments.toOutput { comment ->

View File

@@ -16,8 +16,8 @@ import fr.dcproject.component.vote.entity.VotableImp
import org.joda.time.DateTime
import java.util.UUID
data class CommentForView<T : TargetI, C : CitizenCreatorI>(
override val id: UUID = UUID.randomUUID(),
class CommentForView<T : TargetI, C : CitizenCreatorI>(
id: UUID = UUID.randomUUID(),
override val createdBy: C,
override val target: T,
override var content: String,
@@ -30,7 +30,7 @@ data class CommentForView<T : TargetI, C : CitizenCreatorI>(
CommentWithTargetI<T>,
CreatedBy<C> by CreatedBy.Imp(createdBy),
UpdatedAt by UpdatedAt.Imp(),
DeletedAt,
DeletedAt by DeletedAt.Imp(),
Votable by VotableImp(),
TargetI {
constructor(
@@ -50,9 +50,9 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
override val createdBy: C,
override val target: T,
open var content: String,
override val parent: CommentParentI<T>? = null,
override val parent: CommentParent<T>? = null,
override val deletedAt: DateTime? = null
) : CommentParentI<T> by CommentParent(id, deletedAt, target),
) : CommentParent<T>(id, deletedAt, target),
CommentWithParentI<T>,
ExtraI<T, C>,
CommentWithTargetI<T>,
@@ -62,33 +62,31 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
TargetI {
constructor(
createdBy: C,
parent: CommentParentI<T>,
content: String,
id: UUID? = null,
parent: CommentParent<T>,
content: String
) : this(
createdBy = createdBy,
parent = parent,
target = parent.target,
content = content,
id = id ?: UUID.randomUUID(),
content = content
)
}
data class CommentParent<T : TargetI>(
open class CommentParent<T : TargetI>(
override val id: UUID,
override val deletedAt: DateTime?,
override val target: T
) : CommentRef(id),
CommentParentI<T>
sealed interface CommentParentI<T : TargetI> : CommentI, DeletedAt, CommentWithTargetI<T>
interface CommentParentI<T : TargetI> : CommentI, DeletedAt, CommentWithTargetI<T>
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, HasTarget<T>
interface CommentWithParentI<T : TargetI> {
val parent: CommentParentI<T>?
val parent: CommentParent<T>?
}
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id)
sealed interface CommentI : EntityI
interface CommentI : EntityI

View File

@@ -6,6 +6,7 @@ import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.comment.article.database.CommentArticleRepository
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
@@ -21,7 +22,7 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
): Paginated<CommentForView<T, CitizenCreatorI>>
open fun findByParent(
parent: CommentI,
parent: CommentForView<T, CitizenCreatorI>,
page: Int = 1,
limit: Int = 50
): Paginated<CommentForView<T, CitizenCreatorI>> {
@@ -32,77 +33,94 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
parentId: UUID,
page: Int = 1,
limit: Int = 50
): Paginated<CommentForView<T, CitizenCreatorI>> = requester
.getFunction("find_comments_by_parent")
): Paginated<CommentForView<T, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_parent")
.select<CommentForView<T, CitizenCreator>>(
page,
limit,
"parent_id" to parentId
)
as Paginated<CommentForView<T, CitizenCreatorI>>
}
}
open fun findByTarget(
target: EntityI,
page: Int = 1,
limit: Int = 50,
sort: String = "createdAt"
): Paginated<CommentForView<T, CitizenCreatorI>> = findByTarget(target.id, page, limit, sort)
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
): Paginated<CommentForView<T, CitizenCreatorI>> {
return findByTarget(target.id, page, limit, sort)
}
open fun findByTarget(
targetId: UUID,
page: Int = 1,
limit: Int = 50,
sort: String = "createdAt"
): Paginated<CommentForView<T, CitizenCreatorI>> = requester
.getFunction("find_comments_by_target")
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
): Paginated<CommentForView<T, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_target")
.select<CommentForView<T, CitizenCreator>>(
page,
limit,
"target_id" to targetId,
"sort" to sort
) as Paginated<CommentForView<T, CitizenCreatorI>>
"sort" to sort.sql
)
as Paginated<CommentForView<T, CitizenCreatorI>>
}
}
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>): CommentForView<TargetRef, CitizenCreator> = requester
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>) {
requester
.getFunction("comment")
.selectOne(
.sendQuery(
"reference" to comment.target.reference,
"resource" to comment
)!!
)
}
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>): CommentForView<TargetRef, CitizenCreator> {
return requester
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>) {
requester
.getFunction("edit_comment")
.selectOne(
.sendQuery(
"id" to comment.id,
"content" to comment.content
)!!
)
}
}
class CommentRepository(requester: Requester) : CommentRepositoryAbs<TargetRef>(requester) {
override fun findById(id: UUID): CommentForView<TargetRef, CitizenCreatorI>? = requester
override fun findById(id: UUID): CommentForView<TargetRef, CitizenCreatorI>? {
return requester
.getFunction("find_comment_by_id")
.selectOne<CommentForView<TargetRef, CitizenCreator>>(mapOf("id" to id))
as CommentForView<TargetRef, CitizenCreatorI>?
}
override fun findByCitizen(
citizen: CitizenI,
page: Int,
limit: Int
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> = requester
.getFunction("find_comments_by_citizen")
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select<CommentForView<TargetRef, CitizenCreator>>(
page,
limit,
"created_by_id" to citizen.id
) as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
}
}
override fun findByParent(
parentId: UUID,
page: Int,
limit: Int
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> = requester
.getFunction("find_comments_by_parent")
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_parent")
.select<CommentForView<TargetRef, CitizenCreator>>(
page,
limit,
@@ -110,3 +128,5 @@ class CommentRepository(requester: Requester) : CommentRepositoryAbs<TargetRef>(
)
as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
}
}
}

View File

@@ -1,63 +0,0 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object CreateComment {
@Location("/comments/{comment}")
class CreateCommentRequest(comment: UUID) {
val comment = CommentRef(comment)
class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
}
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
post<CreateCommentRequest> {
mustBeAuth()
call.receiveOrBadRequest<CreateCommentRequest.Input>()
.apply { validate().badRequestIfNotValid() }
.run {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
CommentForUpdate(
content = content,
createdBy = citizen,
target = parent.target,
parent = parent,
)
}.let { newComment ->
ac.canCreate(newComment, citizenOrNull).assert()
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment.toOutput())
}
}
}
}

View File

@@ -0,0 +1,47 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object CreateCommentChildren {
@Location("/comments/{comment}/children")
class CreateCommentChildrenRequest(comment: UUID) {
val comment = CommentRef(comment)
class Input(val content: String)
}
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
post<CreateCommentChildrenRequest> {
mustBeAuth()
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = CommentForUpdate(
content = call.receiveOrBadRequest<CreateCommentChildrenRequest.Input>().content,
createdBy = citizen,
parent = parent
)
ac.assert { canCreate(newComment, citizenOrNull) }
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment.toOutput())
}
}
}

View File

@@ -1,18 +1,14 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
@@ -28,40 +24,22 @@ object EditComment {
@Location("/comments/{comment}")
class EditCommentRequest(comment: UUID) {
val comment = CommentRef(comment)
class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
class Input(val content: String)
}
fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) {
put<EditCommentRequest> {
mustBeAuth()
val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.canUpdate(commentOld, citizenOrNull).assert()
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.assert { canUpdate(comment, citizenOrNull) }
comment.content = call.receiveOrBadRequest<EditCommentRequest.Input>().content
repo.edit(comment)
call.receiveOrBadRequest<EditCommentRequest.Input>()
.apply { validate().badRequestIfNotValid() }
.run {
CommentForUpdate(
id = commentOld.id,
createdBy = commentOld.createdBy,
target = commentOld.target,
parent = commentOld.parent,
content = content,
)
}
.let { repo.edit(it) }
.let {
call.respond(
HttpStatusCode.OK,
it.toOutput()
comment.toOutput()
)
}
}
}
}

View File

@@ -4,7 +4,6 @@ import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import fr.dcproject.routes.PaginatedRequest
@@ -22,13 +21,11 @@ import java.util.UUID
object GetCommentChildren {
@Location("/comments/{comment}/children")
class CommentChildrenRequest(
comment: UUID,
val comment: UUID,
page: Int = 1,
limit: Int = 50,
val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) {
val comment = CommentRef(comment)
}
) : PaginatedRequestI by PaginatedRequest(page, limit)
fun Route.getChildrenComments(repo: CommentRepository, ac: CommentAccessControl) {
get<CommentChildrenRequest> {
@@ -39,7 +36,7 @@ object GetCommentChildren {
it.limit
)
ac.canView(comments.result, citizenOrNull).assert()
ac.assert { canView(comments.result, citizenOrNull) }
call.respond(
HttpStatusCode.OK,

View File

@@ -27,7 +27,7 @@ object GetOneComment {
fun Route.getOneComment(repo: CommentRepository, ac: CommentAccessControl) {
get<CommentRequest> {
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
ac.canView(comment, citizenOrNull).assert()
ac.assert { canView(comment, citizenOrNull) }
call.respond(
HttpStatusCode.OK,

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.component.comment.generic.routes.CreateComment.createCommentChildren
import fr.dcproject.component.comment.generic.routes.CreateCommentChildren.createCommentChildren
import fr.dcproject.component.comment.generic.routes.EditComment.editComment
import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments
import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
@@ -16,9 +15,6 @@ import fr.dcproject.component.constitution.database.ConstitutionForUpdate.TitleF
import fr.dcproject.component.constitution.database.ConstitutionRepository
import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input
import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input.Title
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -40,6 +36,7 @@ object CreateConstitution {
val draft: Boolean = false,
val versionId: UUID = UUID.randomUUID()
) {
class Title(
val id: UUID = UUID.randomUUID(),
val name: String,
@@ -47,25 +44,10 @@ object CreateConstitution {
) {
class ArticleRef(val id: UUID)
}
fun validate() = Validation<Input> {
Input::title {
minLength(10)
maxLength(80)
}
Input::titles onEach {
Title::name {
minLength(10)
maxLength(80)
}
}
}.validate(this)
}
}
private fun getNewConstitution(input: Input, citizen: Citizen) = input.run {
validate().badRequestIfNotValid()
ConstitutionForUpdate<CitizenWithUserI, TitleForUpdate<ArticleRef>>(
id = UUID.randomUUID(),
title = title,
@@ -89,7 +71,7 @@ object CreateConstitution {
post<PostConstitutionRequest> {
mustBeAuth()
getNewConstitution(call.receiveOrBadRequest(), citizen).let {
ac.canCreate(it, citizenOrNull).assert()
ac.assert { canCreate(it, citizenOrNull) }
val c = repo.upsert(it) ?: error("Unable to create Constitution")
call.respond(
HttpStatusCode.Created,

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
@@ -9,10 +8,6 @@ import fr.dcproject.component.constitution.database.ConstitutionRepository
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -32,29 +27,12 @@ object FindConstitutions {
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) {
fun validate() = Validation<FindConstitutionsRequest> {
FindConstitutionsRequest::page {
minimum(1)
}
FindConstitutionsRequest::limit {
minimum(1)
maximum(50)
}
FindConstitutionsRequest::sort ifPresent {
enum(
"title",
"createdAt",
)
}
}.validate(this)
}
) : PaginatedRequestI by PaginatedRequest(page, limit)
fun Route.findConstitutions(repo: ConstitutionRepository, ac: ConstitutionAccessControl) {
get<FindConstitutionsRequest> {
it.validate().badRequestIfNotValid()
val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
ac.canView(constitutions.result, citizenOrNull).assert()
ac.assert { canView(constitutions.result, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
constitutions.toOutput { c ->

View File

@@ -27,7 +27,7 @@ object GetConstitution {
fun Route.getConstitution(ac: ConstitutionAccessControl, constitutionRepo: ConstitutionRepository) {
get<GetConstitutionRequest> {
val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}")
ac.canView(constitution, citizenOrNull).assert()
ac.assert { canView(constitution, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
constitution.let { c ->

View File

@@ -7,8 +7,10 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.response.respondText
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI
@KtorExperimentalAPI
fun Route.definition() {
get("/") {
call.respondText("/openapi.yaml".readResource(), ContentType("text", "yaml"))

View File

@@ -3,8 +3,10 @@ package fr.dcproject.component.doc.routes
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.ExperimentalCoroutinesApi
@KtorExperimentalAPI
@ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI
fun Routing.installDocRoutes() {

View File

@@ -1,13 +1,11 @@
package fr.dcproject.component.follow
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import org.koin.dsl.module
val followKoinModule = module {
single { FollowArticleRepository(get()) }
single { FollowConstitutionRepository(get()) }
single { FollowCitizenRepository(get()) }
single { FollowAccessControl() }
}

View File

@@ -8,18 +8,19 @@ import fr.dcproject.common.entity.HasTarget
import fr.dcproject.common.entity.TargetI
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import java.util.UUID
data class FollowForView<T : TargetI>(
override val id: UUID = UUID.randomUUID(),
open class FollowForView<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenCreator,
override var target: T
) : ExtraI<T, CitizenI>,
) : ExtraI<T, CitizenRef>,
FollowRef(id),
Created<CitizenI> by Created.Imp(createdBy)
Created<CitizenRef> by Created.Imp(createdBy)
data class FollowForUpdate<T : TargetI, C : CitizenI>(
override val id: UUID = UUID.randomUUID(),
class FollowForUpdate<T : TargetI, C : CitizenI>(
id: UUID = UUID.randomUUID(),
override val target: T,
override val createdBy: C
) : FollowRef(id),
@@ -30,4 +31,4 @@ open class FollowRef(
override val id: UUID
) : FollowI
sealed interface FollowI : EntityI
interface FollowI : EntityI

View File

@@ -4,9 +4,7 @@ import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.constitution.database.ConstitutionForView
import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.postgresjson.connexion.Paginated
@@ -74,24 +72,21 @@ sealed class FollowRepository<IN : TargetRef, OUT : TargetRef>(override var requ
target: Entity,
bulkSize: Int = 300
): Flow<FollowForView<IN>> = flow {
var lastId: UUID? = null
while (true) {
val result = findFollowsByTarget(target, lastId, bulkSize)
if (result.count() == 0) {
break
}
result.forEach {
var nextPage = 1
do {
val paginate = findFollowsByTarget(target, nextPage, bulkSize)
paginate.result.forEach {
emit(it)
}
lastId = result.last().id
}
nextPage = paginate.currentPage + 1
} while (!paginate.isLastPage())
}
abstract fun findFollowsByTarget(
target: Entity,
lastId: UUID?,
page: Int = 1,
limit: Int = 300
): List<FollowForView<IN>>
): Paginated<FollowForView<IN>>
}
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
@@ -112,14 +107,14 @@ class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRe
override fun findFollowsByTarget(
target: Entity,
lastId: UUID?,
page: Int,
limit: Int
): List<FollowForView<ArticleRef>> {
): Paginated<FollowForView<ArticleRef>> {
return requester
.getFunction("find_follows_article_by_target")
.select(
"start_id" to lastId,
"limit" to limit,
page,
limit,
"target_id" to target.id
)
}
@@ -143,34 +138,9 @@ class FollowConstitutionRepository(requester: Requester) : FollowRepository<Cons
override fun findFollowsByTarget(
target: Entity,
lastId: UUID?,
limit: Int
): List<FollowForView<ConstitutionRef>> {
TODO("Not yet implemented")
}
}
class FollowCitizenRepository(requester: Requester) : FollowRepository<CitizenRef, Citizen>(requester) {
override fun findByCitizen(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowForView<Citizen>> {
return requester.run {
getFunction("find_follows_citizen_by_citizen")
.select(
page,
limit,
"created_by_id" to citizenId
)
}
}
override fun findFollowsByTarget(
target: Entity,
lastId: UUID?,
limit: Int
): List<FollowForView<CitizenRef>> {
): Paginated<FollowForView<ConstitutionRef>> {
TODO("Not yet implemented")
}
}

View File

@@ -28,7 +28,7 @@ object FollowArticle {
post<ArticleFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.canCreate(follow, citizenOrNull).assert()
ac.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}

View File

@@ -7,7 +7,6 @@ import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.routes.citizen.toOutput
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -15,6 +14,7 @@ import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID
@KtorExperimentalLocationsAPI
@@ -27,10 +27,22 @@ object GetFollowArticle {
fun Route.getFollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
get<ArticleFollowRequest> {
repo.findFollow(citizen, it.article)?.let { follow ->
ac.canView(follow, citizenOrNull).assert()
ac.assert { canView(follow, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
follow.toOutput()
follow.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
)
} ?: call.respond(HttpStatusCode.NoContent)
}

View File

@@ -28,7 +28,7 @@ object GetMyFollowsArticle {
get<CitizenFollowArticleRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.canView(follows.result, citizenOrNull).assert()
ac.assert { canView(follows.result, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->

View File

@@ -28,7 +28,7 @@ object UnfollowArticle {
delete<ArticleFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.canDelete(follow, citizenOrNull).assert()
ac.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}

View File

@@ -1,36 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowForUpdate
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object FollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.followCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
post<CitizenFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
ac.canCreate(follow, citizenOrNull).assert()
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}
}
}

View File

@@ -1,37 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object GetFollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.getFollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
get<CitizenFollowRequest> {
repo.findFollow(citizen, it.citizen)?.let { follow ->
ac.canView(follow, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follow.toOutput()
)
} ?: call.respond(HttpStatusCode.NoContent)
}
}
}

View File

@@ -1,50 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID
@KtorExperimentalLocationsAPI
object GetMyFollowsCitizen {
@Location("/citizens/{citizen}/follows/citizens")
class CitizenFollowCitizenRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.getMyFollowsCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
get<CitizenFollowCitizenRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.canView(follows.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
)
}
}
}

View File

@@ -1,36 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowForUpdate
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.delete
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object UnfollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.unfollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
delete<CitizenFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
ac.canDelete(follow, citizenOrNull).assert()
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}
}
}

View File

@@ -1,20 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.component.follow.routes.citizen.FollowCitizen.followCitizen
import fr.dcproject.component.follow.routes.citizen.GetFollowCitizen.getFollowCitizen
import fr.dcproject.component.follow.routes.citizen.GetMyFollowsCitizen.getMyFollowsCitizen
import fr.dcproject.component.follow.routes.citizen.UnfollowCitizen.unfollowCitizen
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installFollowCitizenRoutes() {
authenticate(optional = true) {
followCitizen(get(), get())
unfollowCitizen(get(), get())
getFollowCitizen(get(), get())
getMyFollowsCitizen(get(), get())
}
}

View File

@@ -1,20 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.component.follow.database.FollowForView
import org.joda.time.DateTime
import java.util.UUID
fun FollowForView<*>.toOutput(): Any = this.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}

View File

@@ -28,7 +28,7 @@ object FollowConstitution {
post<ConstitutionFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.canCreate(follow, citizenOrNull).assert()
ac.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}

View File

@@ -7,7 +7,6 @@ import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.routes.citizen.toOutput
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -15,6 +14,7 @@ import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID
@KtorExperimentalLocationsAPI
@@ -27,10 +27,22 @@ object GetFollowConstitution {
fun Route.getFollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
get<ConstitutionFollowRequest> {
repo.findFollow(citizen, it.constitution)?.let { follow ->
ac.canView(follow, citizenOrNull).assert()
ac.assert { canView(follow, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
follow.toOutput()
follow.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
)
} ?: call.respond(HttpStatusCode.NotFound)
}

View File

@@ -28,7 +28,7 @@ object GetMyFollowsConstitution {
get<CitizenFollowConstitutionRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.canView(follows.result, citizenOrNull).assert()
ac.assert { canView(follows.result, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->

View File

@@ -28,7 +28,7 @@ object UnfollowConstitution {
delete<ConstitutionUnfollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.canDelete(follow, citizenOrNull).assert()
ac.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}

View File

@@ -1,7 +1,5 @@
package fr.dcproject.component.notification
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature
@@ -14,11 +12,7 @@ import fr.dcproject.component.article.database.ArticleForView
import org.joda.time.DateTime
import java.util.concurrent.atomic.AtomicInteger
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
@JsonSubTypes(
JsonSubTypes.Type(value = ArticleUpdateNotificationMessage::class, name = "article")
)
open class NotificationMessage(
open class Notification(
val type: String,
val createdAt: DateTime = DateTime.now()
) {
@@ -48,16 +42,16 @@ open class NotificationMessage(
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
inline fun <reified T : NotificationMessage> fromString(raw: String): T = mapper.readValue(raw)
inline fun <reified T : Notification> fromString(raw: String): T = mapper.readValue(raw)
}
}
open class EntityNotificationMessage <E : Entity> (
open val target: E,
open class EntityNotification(
val target: Entity,
type: String,
val action: String
) : NotificationMessage(type)
) : Notification(type)
data class ArticleUpdateNotificationMessage(
override val target: ArticleForView
) : EntityNotificationMessage<ArticleForView>(target, "article", "update")
class ArticleUpdateNotification(
target: ArticleForView
) : EntityNotification(target, "article", "update")

View File

@@ -0,0 +1,112 @@
package fr.dcproject.component.notification
import com.rabbitmq.client.AMQP.BasicProperties
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import com.rabbitmq.client.ConnectionFactory
import com.rabbitmq.client.Consumer
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.Envelope
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.database.FollowForView
import io.ktor.utils.io.errors.IOException
import io.lettuce.core.RedisClient
import io.lettuce.core.api.async.RedisAsyncCommands
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationConsumer(
private val rabbitFactory: ConnectionFactory,
private val redisClient: RedisClient,
private val followConstitutionRepo: FollowConstitutionRepository,
private val followArticleRepo: FollowArticleRepository,
private val notificationEmailSender: NotificationEmailSender,
private val exchangeName: String,
) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
private val rabbitConnection = rabbitFactory.newConnection()
private val rabbitChannel = rabbitConnection.createChannel()
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
fun close() {
rabbitChannel.close()
rabbitConnection.close()
}
fun start() {
/* Config Rabbit */
rabbitFactory.newConnection().use { connection ->
connection.createChannel().use { channel ->
channel.queueDeclare("push", true, false, false, null)
channel.queueDeclare("email", true, false, false, null)
channel.exchangeDeclare(exchangeName, DIRECT, true)
channel.queueBind("push", exchangeName, "")
channel.queueBind("email", exchangeName, "")
}
}
/* Define Consumer */
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: BasicProperties,
body: ByteArray
) = runBlocking {
followersFromMessage(body) {
redis.zadd(
"notification:${it.follow.createdBy.id}",
it.event.id,
it.rawMessage
)
}
rabbitChannel.basicAck(envelope.deliveryTag, false)
}
}
val consumerEmail: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: BasicProperties,
body: ByteArray
) {
runBlocking {
followersFromMessage(body) {
notificationEmailSender.sendEmail(it.follow)
logger.debug("EmailSend to: ${it.follow.createdBy.id}")
}
}
rabbitChannel.basicAck(envelope.deliveryTag, false)
}
}
/* Launch Consumer */
rabbitChannel.basicConsume("push", false, consumerPush) // The front consume the redis via Websocket
rabbitChannel.basicConsume("email", false, consumerEmail)
}
private suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage) -> Unit) {
val rawMessage: String = body.toString(Charsets.UTF_8)
val notification: EntityNotification = Notification.fromString(rawMessage)
val follows = when (notification.type) {
"article" -> followArticleRepo.findFollowsByTarget(notification.target)
"constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target)
else -> error("event '${notification.type}' not implemented")
}
follows.collect { action(DecodedMessage(notification, rawMessage, it)) }
}
private class DecodedMessage(
val event: EntityNotification,
val rawMessage: String,
val follow: FollowForView<out TargetRef>
)
}

View File

@@ -1,67 +0,0 @@
package fr.dcproject.component.notification
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import com.rabbitmq.client.ConnectionFactory
import com.rabbitmq.client.DefaultConsumer
import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.utils.consumeQueue
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.database.FollowForView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
abstract class NotificationConsumerAbstract(
private val rabbitFactory: ConnectionFactory,
private val followConstitutionRepo: FollowConstitutionRepository,
private val followArticleRepo: FollowArticleRepository,
private val followCitizenRepo: FollowCitizenRepository,
) {
private val rabbitConnection = rabbitFactory.newConnection()
private val rabbitChannel = rabbitConnection.createChannel()
fun close() {
rabbitChannel.close()
rabbitConnection.close()
}
fun declareQueue(queueName: String, exchangeName: String) {
rabbitFactory.newConnection().use { connection ->
connection.createChannel().use { channel ->
channel.queueDeclare(queueName, true, false, false, null)
channel.exchangeDeclare(exchangeName, DIRECT, true)
channel.queueBind(queueName, exchangeName, "")
}
}
}
protected fun consumeQueue(queueName: String, callback: DefaultConsumer.(DecodedMessage<*>) -> Unit) =
rabbitChannel.consumeQueue(queueName) { body ->
runBlocking {
followersFromMessage(body) {
callback(it)
}
}
}
protected suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage<*>) -> Unit) {
val rawMessage: String = body.toString(Charsets.UTF_8)
val notification: EntityNotificationMessage<*> = NotificationMessage.fromString(rawMessage)
val follows = when (notification.type) {
"article" -> followArticleRepo.findFollowsByTarget(notification.target)
"constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target)
"citizen" -> followCitizenRepo.findFollowsByTarget(notification.target)
else -> error("event '${notification.type}' not implemented")
}
follows.collect { action(DecodedMessage(notification, rawMessage, it)) }
}
protected class DecodedMessage <E : Entity> (
val event: EntityNotificationMessage<E>,
val rawMessage: String,
val follow: FollowForView<out TargetRef>
)
}

View File

@@ -0,0 +1,70 @@
package fr.dcproject.component.notification
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.common.email.Mailer
import fr.dcproject.common.entity.EntityI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.article.database.ArticleWithTitleI
import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.component.follow.database.FollowForView
import java.util.UUID
class NotificationEmailSender(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository,
private val articleRepo: ArticleRepository
) {
fun sendEmail(follow: FollowForView<out TargetRef>) {
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
val target = when (follow.target.reference) {
"article" ->
articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
else -> noTarget(follow.target.id)
}
val subject = when (follow.target.reference) {
"article" -> """New version for article "${target.title}""""
else -> "Notification"
}
mailer.sendEmail {
Mail(
Email("notification@$domain"),
subject,
Email(citizen.email),
Content("text/plain", generateContent(citizen, target))
).apply {
addContent(Content("text/html", generateHtmlContent(citizen, target)))
}
}
}
private fun generateHtmlContent(citizen: CitizenCreatorI, target: EntityI): String? {
return when (target) {
is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent()
else -> noTarget(target.id)
}
}
private fun generateContent(citizen: CitizenCreatorI, target: EntityI): String {
return when (target) {
is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent()
else -> noTarget(target.id)
}
}
class NoCitizen(message: String) : Exception(message)
class NoTarget(message: String) : Exception(message)
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
}

View File

@@ -1,9 +1,8 @@
package fr.dcproject.component.notification.push
package fr.dcproject.component.notification
import com.fasterxml.jackson.core.JsonProcessingException
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.notification.NotificationMessage
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.Frame.Text
import io.ktor.http.cio.websocket.readText
@@ -29,42 +28,31 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
/**
* Listen a custom flow to mark as read a message.
*
* And listen the redis subscription flow and call a callback when a new message arrives
*/
class NotificationPushListener(
class NotificationsPush private constructor(
private val redis: RedisAsyncCommands<String, String>,
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
citizen: CitizenI,
incoming: Flow<NotificationMessage>,
onReceive: suspend (NotificationMessage) -> Unit,
incoming: Flow<Notification>,
onRecieve: suspend (Notification) -> Unit,
) {
class Builder(redisClient: RedisClient) {
private val redisConnection = redisClient.connect()
private val redisConnectionPubSub = redisClient.connectPubSub()
private val redis: RedisAsyncCommands<String, String> = redisConnection.async()
class Builder(val redisClient: RedisClient) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis PubSub")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis Async")
/**
* Build Listener with citizen, incoming flow and set an outgoing callback
*/
fun build(
citizen: CitizenI,
incoming: Flow<NotificationMessage>,
onReceive: suspend (NotificationMessage) -> Unit,
): NotificationPushListener = NotificationPushListener(redis, redisConnectionPubSub, citizen, incoming, onReceive)
incoming: Flow<Notification>,
onRecieve: suspend (Notification) -> Unit,
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve)
/**
* Build NotificationPush with only a WebSocket session
*/
@ExperimentalCoroutinesApi
fun build(ws: DefaultWebSocketServerSession): NotificationPushListener {
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
/* Convert channel of string from websocket, to a flow of Notification object */
val incomingFlow: Flow<NotificationMessage> = ws.incoming.consumeAsFlow()
.mapNotNull<Frame, Text> { it as? Text }
val incomingFlow: Flow<Notification> = ws.incoming.consumeAsFlow()
.mapNotNull<Frame, Text> { it as? Frame.Text }
.map { it.readText() }
.map { NotificationMessage.fromString(it) }
.map { Notification.fromString(it) }
return build(ws.call.citizen, incomingFlow) {
ws.outgoing.send(Text(it.toString()))
@@ -74,100 +62,69 @@ class NotificationPushListener(
}
}
/**
* The key of the SortedSet in Redis which contains all the messages of a user
*/
private val key = "notification:${citizen.id}"
/**
* The last score (a kind of sorted ids) of message
*/
private var lastScore: Double = 0.0
/**
* Configure the listener to listen all new notifications
*/
private var score: Double = 0.0
private val listener = object : RedisPubSubAdapter<String, String>() {
/* On new key publish */
override fun message(pattern: String?, channel: String?, message: String?) {
runBlocking {
getNewUnreadNotifications().collect {
onReceive(it)
getNotifications().collect {
onRecieve(it)
}
}
}
}
/**
* Init the listener and the callback
*/
init {
/* Mark as read all incoming notifications */
GlobalScope.launch {
incoming.collect {
it.markAsRead()
markAsRead(it)
}
}
/* Get old notification and sent it to websocket */
runBlocking {
getNewUnreadNotifications().collect {
onReceive(it)
}
getNotifications().collect { onRecieve(it) }
}
/* Listen redis event, and sent the new notification into websocket */
/* Lisen redis event, and sent the new notification into websocket */
redisConnectionPubSub.run {
addListener(listener)
/* Register to the events */
async()?.psubscribe("__key*__:$key")
async()?.psubscribe("__key*__:$key") ?: error("Unable to subscribe to redis events")
}
}
/**
* Close the redis subscription
*/
fun close() {
redisConnectionPubSub.removeListener(listener)
}
/**
* Get All new notification from redis and
* Return flow with notifications
*
* On start, on the first call, this method return all unread notification of the user
*
* Internally this method return all messages that greater of the lastScore,
* then define the lastScore with the score of the last message.
*/
private fun getNewUnreadNotifications() = flow<NotificationMessage> {
/* Return flow with all new notifications */
private fun getNotifications() = flow<Notification> {
redis
.zrangebyscoreWithScores(
key,
Range.from(
Boundary.excluding(lastScore),
Boundary.excluding(score),
Boundary.including(Double.POSITIVE_INFINITY)
),
Limit.from(100)
)
.get().forEach {
/* Build message object from raw string and return it */
emit(NotificationMessage.fromString(it.value))
if (it.score > lastScore) lastScore = it.score
emit(Notification.fromString(it.value))
if (it.score > score) score = it.score
}
}
/**
* Mark one notification as read.
*
* Internally, this method remove the message of the SortedSet in redis
*/
private suspend fun NotificationMessage.markAsRead() = coroutineScope {
private suspend fun markAsRead(notificationMessage: Notification) = coroutineScope {
try {
redis.zremrangebyscore(
key,
Range.from(
Boundary.including(id),
Boundary.including(id)
Boundary.including(notificationMessage.id),
Boundary.including(notificationMessage.id)
)
)
} catch (e: JsonProcessingException) {

View File

@@ -7,15 +7,12 @@ import kotlinx.coroutines.coroutineScope
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationPublisherAsync(
class Publisher(
private val factory: ConnectionFactory,
private val logger: Logger = LoggerFactory.getLogger(NotificationPublisherAsync::class.qualifiedName),
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName),
private val exchangeName: String,
) {
/**
* Publish a new notification message to RabbitMQ
*/
suspend fun <T : EntityNotificationMessage<*>> publishAsync(it: T): Deferred<Unit> = coroutineScope {
suspend fun <T : EntityNotification> publish(it: T): Deferred<Unit> = coroutineScope {
async {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->

View File

@@ -1,33 +0,0 @@
package fr.dcproject.component.notification.email
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.notification.NotificationConsumerAbstract
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationEmailConsumer(
rabbitFactory: ConnectionFactory,
followConstitutionRepo: FollowConstitutionRepository,
followArticleRepo: FollowArticleRepository,
followCitizenRepo: FollowCitizenRepository,
private val notificationEmailSender: NotificationEmailSender,
private val exchangeName: String,
) : NotificationConsumerAbstract(rabbitFactory, followConstitutionRepo, followArticleRepo, followCitizenRepo) {
private val logger: Logger = LoggerFactory.getLogger(NotificationEmailConsumer::class.qualifiedName)
fun start() {
/* Config Rabbit */
declareQueue(QUEUE_NAME, exchangeName)
consumeQueue(QUEUE_NAME) { message ->
notificationEmailSender.sendEmail(message.follow)
logger.debug("EmailSend to: ${message.follow.createdBy.id}")
}
}
companion object {
private const val QUEUE_NAME = "email"
}
}

View File

@@ -1,70 +0,0 @@
package fr.dcproject.component.notification.email
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.common.email.Mailer
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.component.follow.database.FollowForView
import fr.dcproject.component.notification.email.content.ArticleNotificationEmailContent
import fr.dcproject.component.notification.email.content.CitizenNotificationEmailContent
import java.util.UUID
/**
* Send notification email on the follower
*/
class NotificationEmailSender(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository,
private val articleRepo: ArticleRepository
) {
/**
* Send the Notification Email to the follower user
*/
fun sendEmail(follow: FollowForView<out TargetRef>) {
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
/**
* Find the complete target entity by its ID according to its reference
*/
val target = when (follow.target.reference) {
"article" -> articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
"citizen" -> citizenRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
else -> noTarget(follow.target.id)
}
/**
* Find content of the email according to the target type
*/
val content = when (target) {
is ArticleForView -> ArticleNotificationEmailContent(citizen, target, domain)
is Citizen -> CitizenNotificationEmailContent(citizen, target, domain)
else -> noTargetTypeImplementation(follow.target.reference)
}
/* Send email */
mailer.sendEmail {
Mail(
Email("notification@$domain"),
content.subject,
Email(citizen.email),
Content("text/plain", content.content)
).apply {
addContent(Content("text/html", content.contentHtml))
}
}
}
class NoCitizen(message: String) : Exception(message)
class NoTarget(message: String) : Exception(message)
class NoTargetTypeImplement(message: String) : Exception(message)
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
private fun noTargetTypeImplementation(type: String): Nothing = throw NoTargetTypeImplement("No Target type implemented: $type")
}

View File

@@ -1,29 +0,0 @@
package fr.dcproject.component.notification.email.content
import fr.dcproject.component.article.database.ArticleWithTitleI
import fr.dcproject.component.citizen.database.Citizen
data class ArticleNotificationEmailContent(
private val citizen: Citizen,
private val target: ArticleWithTitleI,
private val domain: String,
) : NotificationEmailContent {
override val subject: String
get() = """New version for article "${target.title}""""
override val contentHtml
get() = run {
"""
Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent()
}
override val content
get() = run {
"""
Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent()
}
}

View File

@@ -1,28 +0,0 @@
package fr.dcproject.component.notification.email.content
import fr.dcproject.component.citizen.database.Citizen
data class CitizenNotificationEmailContent(
private val citizen: Citizen,
private val target: Citizen,
private val domain: String,
) : NotificationEmailContent {
override val subject: String
get() = """New activity for the citizen "${target.name}""""
override val contentHtml
get() = run {
"""
Hello ${citizen.name.getFullName()},
The citizen "${target.name}" was new activity, check it here: <a href="http://$domain/citizens/${target.id}">here</a>
""".trimIndent()
}
override val content
get() = run {
"""
Hello ${citizen.name.getFullName()},
The citizen "${target.name}" was new activity, check it here: http://$domain/citizens/${target.id}
""".trimIndent()
}
}

View File

@@ -1,7 +0,0 @@
package fr.dcproject.component.notification.email.content
interface NotificationEmailContent {
val subject: String
val content: String
val contentHtml: String
}

View File

@@ -1,41 +0,0 @@
package fr.dcproject.component.notification.push
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.notification.NotificationConsumerAbstract
import io.lettuce.core.RedisClient
import io.lettuce.core.api.async.RedisAsyncCommands
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationPushConsumer(
rabbitFactory: ConnectionFactory,
followConstitutionRepo: FollowConstitutionRepository,
followArticleRepo: FollowArticleRepository,
followCitizenRepo: FollowCitizenRepository,
redisClient: RedisClient,
private val exchangeName: String,
) : NotificationConsumerAbstract(rabbitFactory, followConstitutionRepo, followArticleRepo, followCitizenRepo) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
private val logger: Logger = LoggerFactory.getLogger(NotificationPushConsumer::class.qualifiedName)
fun start() {
/* Config Rabbit */
declareQueue(QUEUE_NAME, exchangeName)
consumeQueue(QUEUE_NAME) { message ->
redis.zadd(
"notification:${message.follow.createdBy.id}",
message.event.id,
message.rawMessage
)
logger.debug("Notification was transferred to the redis (follower: ${message.follow.createdBy.id})")
}
}
companion object {
private const val QUEUE_NAME = "push"
}
}

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.notification.routes
import fr.dcproject.component.notification.push.NotificationPushListener
import fr.dcproject.component.notification.NotificationsPush
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Route
import io.ktor.websocket.webSocket
@@ -13,8 +13,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
*/
@ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI
fun Route.notificationArticle(pushListenerBuilder: NotificationPushListener.Builder) {
fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) {
webSocket("/notifications") {
pushListenerBuilder.build(this)
pushBuilder.build(this)
}
}

View File

@@ -13,8 +13,8 @@ import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import java.util.UUID
data class Opinion<T : TargetI>(
override val id: UUID = UUID.randomUUID(),
open class Opinion<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenCreator,
override val target: T,
val choice: OpinionChoice
@@ -39,4 +39,4 @@ open class OpinionRef(
override val id: UUID
) : OpinionI, TargetRef(id)
sealed interface OpinionI : EntityI
interface OpinionI : EntityI

View File

@@ -7,4 +7,6 @@ interface Opinionable {
val opinions: Opinions
}
data class OpinionableImp(override var opinions: OpinionsMutable = mutableMapOf()) : Opinionable
class OpinionableImp : Opinionable {
override var opinions: OpinionsMutable = mutableMapOf()
}

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