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 name: Tests
on: on:
push:
branches:
- 'master'
pull_request: pull_request:
branches: branches:
- 'master' - 'master'
@@ -21,10 +18,6 @@ jobs:
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 11
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
- name: Cache Gradle packages - name: Cache Gradle packages
uses: actions/cache@v2 uses: actions/cache@v2
@@ -36,17 +29,26 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-gradle- ${{ runner.os }}-gradle-
- name: Build - name: Build
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt 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 - name: processResources
run: gradle processResources uses: eskatos/gradle-command-action@v1
- name: processTestResources
uses: gradle/gradle-build-action@v2
with: 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 arguments: processResources
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
@@ -67,17 +69,10 @@ jobs:
with: with:
name: Build name: Build
path: build path: build
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testSqlComposeUp
- name: TestSql - name: TestSql
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: testSql arguments: testSql
test: test:
@@ -86,58 +81,37 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 11
- uses: actions/download-artifact@v2 - uses: actions/download-artifact@v2
with: with:
name: Build name: Build
path: build path: build
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testComposeUp
- name: Test - name: Test
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: test arguments: test -x testSql
- name: Coverage - name: Coverage
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: coveralls arguments: coveralls
env: env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
- name: Cache SonarCloud packages - name: Cache SonarCloud packages
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
path: ~/.sonar/cache path: ~/.sonar/cache
key: ${{ runner.os }}-sonar key: ${{ runner.os }}-sonar
restore-keys: ${{ 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 - name: Build and analyze
uses: gradle/gradle-build-action@v2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with: run: ./gradlew build sonarqube --info
gradle-version: '7.4'
arguments: sonarqube --info
lint: lint:
needs: build needs: build
@@ -154,7 +128,7 @@ jobs:
name: Build name: Build
path: build path: build
- name: Lint - name: Lint
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: ktlintCheck arguments: ktlintCheck

View File

@@ -8,7 +8,7 @@
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<SqlCodeStyleSettings version="6"> <SqlCodeStyleSettings version="5">
<option name="KEYWORD_CASE" value="1" /> <option name="KEYWORD_CASE" value="1" />
<option name="IDENTIFIER_CASE" value="1" /> <option name="IDENTIFIER_CASE" value="1" />
<option name="TYPE_CASE" value="4" /> <option name="TYPE_CASE" value="4" />
@@ -56,13 +56,21 @@
</indentOptions> </indentOptions>
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="kotlin"> <codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" /> <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" /> <option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" /> <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" /> <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> <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> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions> </indentOptions>

View File

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

View File

@@ -4,7 +4,7 @@
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <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"> <option name="taskDescriptions">
<list /> <list />
</option> </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="rabbitmq" />
<option value="redis" /> <option value="redis" />
<option value="openapi" /> <option value="openapi" />
<option value="sonarqube" />
<option value="sonarqube_db" />
</list> </list>
</option> </option>
<option name="sourceFilePath" value="docker-compose.yml" /> <option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings> </settings>
</deployment> </deployment>
<method v="2" /> <method v="2" />

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <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> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <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 # 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) [![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) [![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.owasp.dependencycheck.reporting.ReportGenerator
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
val ktorVersion = "1.5.4" val ktorVersion = "1.5.0"
val kotlinVersion = "1.5.31" val kotlinVersion = "1.4.30"
val coroutinesVersion = "1.5.2" val coroutinesVersion = "1.4.3"
val logbackVersion = "1.2.3" val logbackVersion = "1.2.3"
val koinVersion = "3.1.5" val koinVersion = "2.0.1"
val jacksonVersion = "2.13.1" val jacksonVersion = "2.12.1"
group = "com.github.flecomte" group = "com.github.flecomte"
version = versioning.info.run { version = versioning.info.run {
@@ -28,24 +28,20 @@ version = versioning.info.run {
plugins { plugins {
jacoco jacoco
application application
`maven-publish` maven
kotlin("jvm") version "1.5.31" id("maven-publish")
kotlin("plugin.serialization") version "1.5.31" kotlin("jvm") version "1.4.30"
kotlin("plugin.serialization") version "1.4.30"
id("com.github.johnrengelman.shadow") version "7.1.2" id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jlleitschuh.gradle.ktlint") version "10.2.1" id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
id("org.owasp.dependencycheck") version "6.1.5" id("org.owasp.dependencycheck") version "6.1.1"
id("org.sonarqube") version "3.3" id("org.sonarqube") version "3.1.1"
id("net.nemerosa.versioning") version "2.15.1" id("net.nemerosa.versioning") version "2.14.0"
id("io.gitlab.arturbosch.detekt") version "1.19.0" id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
id("com.avast.gradle.docker-compose") version "0.15.1" id("com.avast.gradle.docker-compose") version "0.14.0"
id("com.github.kt3k.coveralls") version "2.12.0" id("com.github.kt3k.coveralls") version "2.8.4"
}
dependencyLocking {
lockAllConfigurations()
// lockMode.set(LockMode.STRICT)
} }
application { application {
@@ -60,7 +56,7 @@ buildscript {
maven { url = uri("https://jitpack.io") } maven { url = uri("https://jitpack.io") }
} }
dependencies { dependencies {
classpath("com.typesafe:config:1.4.2") classpath("com.typesafe:config:1.4.1")
classpath("com.github.flecomte:postgres-json:2.1.2") classpath("com.github.flecomte:postgres-json:2.1.2")
} }
} }
@@ -98,7 +94,7 @@ val migration by tasks.registering {
} }
val migrationTest by tasks.registering { val migrationTest by tasks.registering {
group = "tests" group = "verification"
dependsOn(tasks.named("testComposeUp")) dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown")) finalizedBy(tasks.named("testComposeDown"))
doLast { doLast {
@@ -122,9 +118,11 @@ val migrationTest by tasks.registering {
} }
val testSql by tasks.registering { val testSql by tasks.registering {
group = "tests" group = "verification"
dependsOn(tasks.named("processResources")) dependsOn(tasks.named("processResources"))
dependsOn(tasks.named("processTestResources")) dependsOn(tasks.named("processTestResources"))
dependsOn(tasks.named("testSqlComposeUp"))
finalizedBy(tasks.named("testSqlComposeDown"))
doLast { doLast {
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve() val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
@@ -169,7 +167,6 @@ tasks.withType<Jar> {
) )
) )
} }
isZip64 = true
} }
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
@@ -183,10 +180,11 @@ tasks.withType<KotlinCompile> {
tasks.named<ShadowJar>("shadowJar") { tasks.named<ShadowJar>("shadowJar") {
mergeServiceFiles("META-INF/services") mergeServiceFiles("META-INF/services")
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}") archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}")
isZip64 = true
} }
tasks.sonarqube.configure { tasks.sonarqube.configure {
dependsOn(tasks.test)
dependsOn(tasks.detekt)
dependsOn(tasks.jacocoTestReport) dependsOn(tasks.jacocoTestReport)
} }
@@ -200,6 +198,7 @@ tasks.test {
useJUnit() useJUnit()
useJUnitPlatform() useJUnitPlatform()
systemProperty("junit.jupiter.execution.parallel.enabled", true) systemProperty("junit.jupiter.execution.parallel.enabled", true)
dependsOn(testSql)
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
} }
@@ -207,6 +206,13 @@ coveralls {
sourceDirs.add("src/main/kotlin") sourceDirs.add("src/main/kotlin")
} }
tasks.register("testAll") {
group = "verification"
dependsOn(testSql)
dependsOn(tasks.test)
dependsOn(tasks.ktlintCheck)
}
apply(plugin = "docker-compose") apply(plugin = "docker-compose")
dockerCompose { dockerCompose {
projectName = "dc-project" projectName = "dc-project"
@@ -222,12 +228,14 @@ dockerCompose {
useComposeFiles = listOf("docker-compose-test.yml") useComposeFiles = listOf("docker-compose-test.yml")
startedServices = listOf("db", "elasticsearch") startedServices = listOf("db", "elasticsearch")
stopContainers = false stopContainers = false
isRequiredBy(project.tasks.named("testSql"))
} }
createNested("test").apply { createNested("test").apply {
projectName = "dc-project_test" projectName = "dc-project_test"
useComposeFiles = listOf("docker-compose-test.yml") useComposeFiles = listOf("docker-compose-test.yml")
stopContainers = false stopContainers = false
isRequiredBy(project.tasks.test)
} }
} }
@@ -258,7 +266,7 @@ publishing {
} }
jacoco { jacoco {
toolVersion = "0.8.7" toolVersion = "0.8.6"
applyTo(tasks.run.get()) 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 { dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML) formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
} }
@@ -388,6 +327,7 @@ dependencyCheck {
repositories { repositories {
mavenLocal() mavenLocal()
jcenter() jcenter()
maven { url = uri("https://kotlin.bintray.com/ktor") }
maven { url = uri("https://jitpack.io") } maven { url = uri("https://jitpack.io") }
} }
@@ -405,7 +345,7 @@ dependencies {
implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktorVersion") implementation("io.ktor:ktor-auth-jwt:$ktorVersion")
implementation("io.ktor:ktor-websockets:$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("io.ktor:ktor-jackson:$ktorVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$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("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
implementation("com.jayway.jsonpath:json-path:2.5.0") implementation("com.jayway.jsonpath:json-path:2.5.0")
implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.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-server-tests:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock:$ktorVersion") testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock-jvm:$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.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.amshove.kluent:kluent:1.68") testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
testImplementation("io.mockk:mockk:1.12.2") testImplementation("org.amshove.kluent:kluent:1.61")
testImplementation("io.mockk:mockk-agent-api:1.12.2") testImplementation("io.mockk:mockk-agent-api:1.10.6")
testImplementation("io.mockk:mockk-agent-jvm:1.12.2") testImplementation("io.mockk:mockk-agent-jvm:1.10.6")
testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
testImplementation("com.thedeanda:lorem:2.1") testImplementation("com.thedeanda:lorem:2.1")
testImplementation("org.openapi4j:openapi-operation-validator:1.0.6") 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: services:
rabbitmq: rabbitmq:
container_name: ${APP_NAME}_rabbitmq_test 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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.PROD
import fr.dcproject.application.Env.TEST import fr.dcproject.application.Env.TEST
import fr.dcproject.application.http.statusPagesInstallation import fr.dcproject.application.http.statusPagesInstallation
import fr.dcproject.common.utils.onApplicationStopped
import fr.dcproject.component.article.articleKoinModule import fr.dcproject.component.article.articleKoinModule
import fr.dcproject.component.article.routes.installArticleRoutes import fr.dcproject.component.article.routes.installArticleRoutes
import fr.dcproject.component.auth.authKoinModule 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.doc.routes.installDocRoutes
import fr.dcproject.component.follow.followKoinModule import fr.dcproject.component.follow.followKoinModule
import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes 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.follow.routes.constitution.installFollowConstitutionRoutes
import fr.dcproject.component.notification.email.NotificationEmailConsumer import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.push.NotificationPushConsumer
import fr.dcproject.component.notification.routes.installNotificationsRoutes import fr.dcproject.component.notification.routes.installNotificationsRoutes
import fr.dcproject.component.opinion.opinionKoinModule import fr.dcproject.component.opinion.opinionKoinModule
import fr.dcproject.component.opinion.routes.installOpinionRoutes 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.dcproject.component.workgroup.workgroupKoinModule
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
import io.ktor.application.install import io.ktor.application.install
import io.ktor.auth.Authentication import io.ktor.auth.Authentication
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
@@ -59,6 +57,7 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations import io.ktor.locations.Locations
import io.ktor.routing.Routing import io.ktor.routing.Routing
import io.ktor.server.jetty.EngineMain import io.ktor.server.jetty.EngineMain
import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog 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 } enum class Env { PROD, TEST }
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf @Suppress("unused") // Referenced in application.conf
fun Application.module(env: Env = PROD) { fun Application.module(env: Env = PROD) {
@@ -117,14 +117,11 @@ fun Application.module(env: Env = PROD) {
masking = false masking = false
} }
get<NotificationEmailConsumer>().run { get<NotificationConsumer>().run {
start() start()
onApplicationStopped { close() } environment.monitor.subscribe(ApplicationStopped) {
close()
} }
get<NotificationPushConsumer>().run {
start()
onApplicationStopped { close() }
} }
install(Authentication, jwtInstallation(get(), get())) install(Authentication, jwtInstallation(get(), get()))
@@ -157,7 +154,6 @@ fun Application.module(env: Env = PROD) {
installCommentRoutes() installCommentRoutes()
installFollowArticleRoutes() installFollowArticleRoutes()
installFollowConstitutionRoutes() installFollowConstitutionRoutes()
installFollowCitizenRoutes()
installWorkgroupRoutes() installWorkgroupRoutes()
installOpinionRoutes() installOpinionRoutes()
installVoteRoutes() installVoteRoutes()

View File

@@ -1,32 +1,23 @@
package fr.dcproject.application 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.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 import java.util.UUID
private typealias ConverterDeclaration = DataConversion.Configuration.() -> Unit 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 = { val converters: ConverterDeclaration = {
convert<UUID> { convert<UUID> {
decode { values, _ -> decode { values, _ ->
try {
values.singleOrNull()?.let { UUID.fromString(it) } values.singleOrNull()?.let { UUID.fromString(it) }
} catch (e: Throwable) {
throw BadRequestException(
HttpErrorBadRequest(
HttpStatusCode.BadRequest,
invalidParams = listOf(
InvalidParam(
"ID",
"must be UUID"
)
)
)
)
}
} }
encode { value -> encode { value ->

View File

@@ -10,20 +10,21 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.common.email.Mailer import fr.dcproject.common.email.Mailer
import fr.dcproject.component.auth.jwt.JwtConfig import fr.dcproject.component.auth.jwt.JwtConfig
import fr.dcproject.component.notification.NotificationPublisherAsync import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.email.NotificationEmailConsumer import fr.dcproject.component.notification.NotificationEmailSender
import fr.dcproject.component.notification.email.NotificationEmailSender import fr.dcproject.component.notification.NotificationsPush
import fr.dcproject.component.notification.push.NotificationPushConsumer import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.notification.push.NotificationPushListener
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
@KtorExperimentalAPI
val KoinModule = module { val KoinModule = module {
// JWT // JWT
single { single {
@@ -64,15 +65,11 @@ val KoinModule = module {
} }
} }
single { NotificationPushListener.Builder(get()) } single { NotificationsPush.Builder(get()) }
single { single {
val config: Configuration = get() val config: Configuration = get()
NotificationEmailConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName) NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
}
single {
val config: Configuration = get()
NotificationPushConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
} }
// RabbitMQ // RabbitMQ
@@ -117,7 +114,7 @@ val KoinModule = module {
single { single {
val config: Configuration = get() val config: Configuration = get()
NotificationPublisherAsync(factory = get(), exchangeName = config.exchangeNotificationName) Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
} }
single { 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 fr.dcproject.component.auth.user
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.features.ParameterConversionException
import io.ktor.features.StatusPages import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.response.respond import io.ktor.response.respond
@@ -14,10 +13,18 @@ import java.util.concurrent.CompletionException
class HttpError( class HttpError(
statusCode: HttpStatusCode, statusCode: HttpStatusCode,
cause: Throwable? = null, val cause: Throwable? = null,
val type: String? = null,
val title: String = cause?.message ?: statusCode.description, 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 val statusCode: Int = statusCode.value
data class InvalidParam(
val name: String,
val reason: String
)
} }
fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = { fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = {
@@ -72,15 +79,4 @@ fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = {
call.respond(HttpStatusCode.Forbidden, it) 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( class Mailer(
private val key: String private val key: String
) { ) {
/**
* Send email via Sendgrid
*/
fun sendEmail(action: () -> Mail): Boolean { fun sendEmail(action: () -> Mail): Boolean {
val mail = action() 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.article.database.ArticleRef
import fr.dcproject.component.citizen.database.CitizenI 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.comment.generic.database.CommentRef
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.opinion.database.OpinionRef import fr.dcproject.component.opinion.database.OpinionRef
@@ -35,8 +34,7 @@ interface TargetI : EntityI {
Article("article"), Article("article"),
Constitution("constitution"), Constitution("constitution"),
Comment("comment"), Comment("comment"),
Opinion("opinion"), Opinion("opinion")
Citizen("citizen"),
} }
companion object { companion object {
@@ -46,7 +44,6 @@ interface TargetI : EntityI {
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.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") 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( open class PaginatedRequest(
override val page: Int = 1, page: Int = 1,
override val limit: Int = 50 limit: Int = 50
) : PaginatedRequestI ) : 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 com.jayway.jsonpath.PathNotFoundException
import org.apache.http.util.EntityUtils import org.apache.http.util.EntityUtils
import org.elasticsearch.client.Response import org.elasticsearch.client.Response
import org.slf4j.LoggerFactory
fun Response.contentToString(): String { fun Response.contentToString(): String {
return EntityUtils.toString(this.entity) return EntityUtils.toString(this.entity)
@@ -21,6 +22,8 @@ fun String.getJsonField(jsonPath: String): Int? {
return try { return try {
JsonPath.read(this, jsonPath) JsonPath.read(this, jsonPath)
} catch (e: PathNotFoundException) { } catch (e: PathNotFoundException) {
LoggerFactory.getLogger("fr.dcproject.utils.getJsonField")
.warn("No value for Json path ${JsonPath.compile(jsonPath).path}")
null 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) { if (subject.createdBy.id == citizen.id) {
/* The creator must be the same of the creator of preview version of article */ /* The creator must be the same of the creator of preview version of article */
val lastVersionId = articleRepo val lastVersionId = articleRepo
.findSiblingVersions(1, 1, subject) .findVersionsByVersionId(1, 1, subject.versionId)
.result .result
.firstOrNull()?.createdBy?.id .firstOrNull()?.createdBy?.id

View File

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

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.article.database package fr.dcproject.component.article.database
import fr.dcproject.common.entity.VersionableId
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter import fr.postgresjson.entity.Parameter
@@ -20,10 +19,10 @@ class ArticleRepository(override var requester: Requester) : RepositoryI {
.select(page, limit, "id" to id) .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 return requester
.getFunction("find_articles_versions_by_version_id") .getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to article.versionId) .select(page, limit, "version_id" to versionId)
} }
fun find( fun find(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,7 @@
package fr.dcproject.component.auth.routes package fr.dcproject.component.auth.routes
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.utils.receiveOrBadRequest 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.UserForCreate
import fr.dcproject.component.auth.database.UserI import fr.dcproject.component.auth.database.UserI
import fr.dcproject.component.auth.jwt.makeToken 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.CitizenForCreate
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRepository 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.application.call
import io.ktor.features.BadRequestException import io.ktor.features.BadRequestException
import io.ktor.http.ContentType import io.ktor.http.ContentType
@@ -49,35 +43,6 @@ object Register {
val username: String, val username: String,
val password: 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( user = UserForCreate(
username = user.username, username = user.username,
password = user.password, password = user.password,
roles = setOf(UserI.Roles.ROLE_USER) roles = listOf(UserI.Roles.ROLE_USER)
) )
) )
post<RegisterRequest> { post<RegisterRequest> {
try { try {
val citizen = call.receiveOrBadRequest<Input>() val citizen = call.receiveOrBadRequest<Input>().toCitizen()
.apply { validate().badRequestIfNotValid() }
.toCitizen()
citizenRepo.insertWithUser(citizen)?.user?.makeToken()?.let { token -> citizenRepo.insertWithUser(citizen)?.user?.makeToken()?.let { token ->
if (call.request.accept() == ContentType.Application.Json.toString()) { if (call.request.accept() == ContentType.Application.Json.toString()) {
call.respond( 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.CreatedAt
import fr.dcproject.common.entity.DeletedAt import fr.dcproject.common.entity.DeletedAt
import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.EntityI 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.User
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserForCreate import fr.dcproject.component.auth.database.UserForCreate
@@ -17,19 +16,19 @@ import fr.postgresjson.entity.Serializable
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.UUID import java.util.UUID
data class CitizenForCreate( class CitizenForCreate(
val name: Name, val name: Name,
val email: String, val email: String,
val birthday: DateTime, val birthday: DateTime,
val voteAnonymous: Boolean = true, val voteAnonymous: Boolean = true,
val followAnonymous: Boolean = true, val followAnonymous: Boolean = true,
override val user: UserForCreate, override val user: UserForCreate,
override val id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
) : CitizenI, ) : CitizenI,
CitizenWithUserI by CitizenRefWithUser(id, user), CitizenRefWithUser(id, user),
CreatedAt by CreatedAt.Imp() CreatedAt by CreatedAt.Imp()
data class Citizen( class Citizen(
override val id: UUID = UUID.randomUUID(), override val id: UUID = UUID.randomUUID(),
override val name: Name, override val name: Name,
override val email: String, override val email: String,
@@ -37,7 +36,7 @@ data class Citizen(
override val voteAnonymous: Boolean = true, override val voteAnonymous: Boolean = true,
override val followAnonymous: Boolean = true, override val followAnonymous: Boolean = true,
override val user: User, override val user: User,
override val deletedAt: DateTime? = null deletedAt: DateTime? = null
) : CitizenWithEmail, ) : CitizenWithEmail,
CitizenCreatorI, CitizenCreatorI,
CitizenWithUserI, CitizenWithUserI,
@@ -62,11 +61,10 @@ data class CitizenCreator(
override val user: UserCreator, override val user: UserCreator,
override val deletedAt: DateTime? = null override val deletedAt: DateTime? = null
) : CitizenCreatorI, ) : CitizenCreatorI,
CitizenI, CitizenRefWithUser(id, user),
CitizenWithUserI by CitizenRefWithUser(id, user),
DeletedAt by DeletedAt.Imp(deletedAt) DeletedAt by DeletedAt.Imp(deletedAt)
sealed interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, DeletedAt { interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, DeletedAt {
override val id: UUID override val id: UUID
override val name: Name override val name: Name
override val email: String override val email: String
@@ -76,8 +74,8 @@ sealed interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCa
override val deletedAt: DateTime? override val deletedAt: DateTime?
} }
data class CitizenCart( class CitizenCart(
override val id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val name: Name, override val name: Name,
override val user: UserRef, override val user: UserRef,
override val deletedAt: DateTime? = null, override val deletedAt: DateTime? = null,
@@ -85,22 +83,22 @@ data class CitizenCart(
CitizenCartI, CitizenCartI,
DeletedAt by DeletedAt.Imp(deletedAt) DeletedAt by DeletedAt.Imp(deletedAt)
sealed interface CitizenCartI : CitizenI, CitizenWithUserI { interface CitizenCartI : CitizenI, CitizenWithUserI {
val name: Name val name: Name
} }
data class CitizenRefWithUser( open class CitizenRefWithUser(
override val id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val user: UserI override val user: UserRef
) : CitizenWithUserI, ) : CitizenWithUserI,
CitizenRef(id) CitizenRef(id)
open class CitizenRef( open class CitizenRef(
id: UUID = UUID.randomUUID() id: UUID = UUID.randomUUID()
) : TargetRef(id), ) : Entity(id),
CitizenI CitizenI
sealed interface CitizenI : EntityI, TargetI { interface CitizenI : EntityI {
data class Name( data class Name(
override val firstName: String, override val firstName: String,
override val lastName: String, override val lastName: String,
@@ -115,10 +113,10 @@ sealed interface CitizenI : EntityI, TargetI {
} }
} }
sealed interface CitizenWithUserI : CitizenI { interface CitizenWithUserI : CitizenI {
val user: UserI val user: UserI
} }
sealed interface CitizenWithEmail : CitizenI { interface CitizenWithEmail : CitizenI {
val email: String val email: String
} }

View File

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

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.citizen.routes package fr.dcproject.component.citizen.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull 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.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI 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.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
@@ -32,30 +27,13 @@ object FindCitizens {
val sort: String? = null, val sort: String? = null,
val direction: RepositoryI.Direction? = null, val direction: RepositoryI.Direction? = null,
val search: String? = null val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) : 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)
}
fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) { fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) {
get<CitizensRequest> { get<CitizensRequest> {
mustBeAuth() mustBeAuth()
it.validate().badRequestIfNotValid()
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) 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( call.respond(
citizens.toOutput { c: CitizenCreator -> citizens.toOutput { c: CitizenCreator ->
object { object {

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
target: EntityI, target: EntityI,
page: Int, page: Int,
limit: Int, limit: Int,
sort: String sort: Sort
): Paginated<CommentForView<ArticleForView, CitizenCreatorI>> { ): Paginated<CommentForView<ArticleForView, CitizenCreatorI>> {
return requester return requester
.getFunction("find_comments_by_target") .getFunction("find_comments_by_target")
@@ -49,7 +49,18 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
page, page,
limit, limit,
"target_id" to target.id, "target_id" to target.id,
"sort" to sort "sort" to sort.sql
) as Paginated<CommentForView<ArticleForView, CitizenCreatorI>> ) 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 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.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef 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.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.toOutput 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.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -29,29 +26,20 @@ object CreateCommentArticle {
@Location("/articles/{article}/comments") @Location("/articles/{article}/comments")
class PostArticleCommentRequest(article: UUID) { class PostArticleCommentRequest(article: UUID) {
val article = ArticleRef(article) val article = ArticleRef(article)
class Input(val content: String) { class Input(val content: String)
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
} }
fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) { fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) {
post<PostArticleCommentRequest> { post<PostArticleCommentRequest> {
mustBeAuth() mustBeAuth()
call.receiveOrBadRequest<Input>() call.receiveOrBadRequest<Input>().run {
.apply { validate().badRequestIfNotValid() }
.run {
CommentForUpdate( CommentForUpdate(
target = it.article, target = it.article,
createdBy = citizen, createdBy = citizen,
content = content content = content
) )
}.let { comment -> }.let { comment ->
ac.canCreate(comment, citizenOrNull).assert() ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment) repo.comment(comment)
call.respond( call.respond(

View File

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

View File

@@ -28,7 +28,7 @@ object GetCitizenArticleComments {
get<CitizenCommentArticleRequest> { get<CitizenCommentArticleRequest> {
mustBeAuth() mustBeAuth()
repo.findByCitizen(it.citizen).let { comments -> repo.findByCitizen(it.citizen).let { comments ->
ac.canView(comments.result, citizenOrNull).assert() ac.assert { canView(comments.result, citizenOrNull) }
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,
comments.toOutput { comment -> 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.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenCreatorI import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenI 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.CommentForView
import fr.dcproject.component.comment.generic.database.CommentRepositoryAbs import fr.dcproject.component.comment.generic.database.CommentRepositoryAbs
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
@@ -40,7 +41,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
target: EntityI, target: EntityI,
page: Int, page: Int,
limit: Int, limit: Int,
sort: String sort: CommentArticleRepository.Sort
): Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> { ): Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> {
return requester.run { return requester.run {
getFunction("find_comments_by_target") getFunction("find_comments_by_target")
@@ -48,7 +49,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
page, page,
limit, limit,
"target_id" to target.id, "target_id" to target.id,
"sort" to sort "sort" to sort.sql
) )
as Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> as Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>>
} }

View File

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

View File

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

View File

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

View File

@@ -16,8 +16,8 @@ import fr.dcproject.component.vote.entity.VotableImp
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.UUID import java.util.UUID
data class CommentForView<T : TargetI, C : CitizenCreatorI>( class CommentForView<T : TargetI, C : CitizenCreatorI>(
override val id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val createdBy: C, override val createdBy: C,
override val target: T, override val target: T,
override var content: String, override var content: String,
@@ -30,7 +30,7 @@ data class CommentForView<T : TargetI, C : CitizenCreatorI>(
CommentWithTargetI<T>, CommentWithTargetI<T>,
CreatedBy<C> by CreatedBy.Imp(createdBy), CreatedBy<C> by CreatedBy.Imp(createdBy),
UpdatedAt by UpdatedAt.Imp(), UpdatedAt by UpdatedAt.Imp(),
DeletedAt, DeletedAt by DeletedAt.Imp(),
Votable by VotableImp(), Votable by VotableImp(),
TargetI { TargetI {
constructor( constructor(
@@ -50,9 +50,9 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
override val createdBy: C, override val createdBy: C,
override val target: T, override val target: T,
open var content: String, open var content: String,
override val parent: CommentParentI<T>? = null, override val parent: CommentParent<T>? = null,
override val deletedAt: DateTime? = null override val deletedAt: DateTime? = null
) : CommentParentI<T> by CommentParent(id, deletedAt, target), ) : CommentParent<T>(id, deletedAt, target),
CommentWithParentI<T>, CommentWithParentI<T>,
ExtraI<T, C>, ExtraI<T, C>,
CommentWithTargetI<T>, CommentWithTargetI<T>,
@@ -62,33 +62,31 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
TargetI { TargetI {
constructor( constructor(
createdBy: C, createdBy: C,
parent: CommentParentI<T>, parent: CommentParent<T>,
content: String, content: String
id: UUID? = null,
) : this( ) : this(
createdBy = createdBy, createdBy = createdBy,
parent = parent, parent = parent,
target = parent.target, target = parent.target,
content = content, content = content
id = id ?: UUID.randomUUID(),
) )
} }
data class CommentParent<T : TargetI>( open class CommentParent<T : TargetI>(
override val id: UUID, override val id: UUID,
override val deletedAt: DateTime?, override val deletedAt: DateTime?,
override val target: T override val target: T
) : CommentRef(id), ) : CommentRef(id),
CommentParentI<T> 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 CommentWithTargetI<T : TargetI> : CommentI, TargetI, HasTarget<T>
interface CommentWithParentI<T : TargetI> { interface CommentWithParentI<T : TargetI> {
val parent: CommentParentI<T>? val parent: CommentParent<T>?
} }
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id) 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.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenCreatorI import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.comment.article.database.CommentArticleRepository
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
@@ -21,7 +22,7 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
): Paginated<CommentForView<T, CitizenCreatorI>> ): Paginated<CommentForView<T, CitizenCreatorI>>
open fun findByParent( open fun findByParent(
parent: CommentI, parent: CommentForView<T, CitizenCreatorI>,
page: Int = 1, page: Int = 1,
limit: Int = 50 limit: Int = 50
): Paginated<CommentForView<T, CitizenCreatorI>> { ): Paginated<CommentForView<T, CitizenCreatorI>> {
@@ -32,77 +33,94 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
parentId: UUID, parentId: UUID,
page: Int = 1, page: Int = 1,
limit: Int = 50 limit: Int = 50
): Paginated<CommentForView<T, CitizenCreatorI>> = requester ): Paginated<CommentForView<T, CitizenCreatorI>> {
.getFunction("find_comments_by_parent") return requester.run {
getFunction("find_comments_by_parent")
.select<CommentForView<T, CitizenCreator>>( .select<CommentForView<T, CitizenCreator>>(
page, page,
limit, limit,
"parent_id" to parentId "parent_id" to parentId
) )
as Paginated<CommentForView<T, CitizenCreatorI>> as Paginated<CommentForView<T, CitizenCreatorI>>
}
}
open fun findByTarget( open fun findByTarget(
target: EntityI, target: EntityI,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
sort: String = "createdAt" sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
): Paginated<CommentForView<T, CitizenCreatorI>> = findByTarget(target.id, page, limit, sort) ): Paginated<CommentForView<T, CitizenCreatorI>> {
return findByTarget(target.id, page, limit, sort)
}
open fun findByTarget( open fun findByTarget(
targetId: UUID, targetId: UUID,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
sort: String = "createdAt" sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
): Paginated<CommentForView<T, CitizenCreatorI>> = requester ): Paginated<CommentForView<T, CitizenCreatorI>> {
.getFunction("find_comments_by_target") return requester.run {
getFunction("find_comments_by_target")
.select<CommentForView<T, CitizenCreator>>( .select<CommentForView<T, CitizenCreator>>(
page, page,
limit, limit,
"target_id" to targetId, "target_id" to targetId,
"sort" to sort "sort" to sort.sql
) as Paginated<CommentForView<T, CitizenCreatorI>> )
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") .getFunction("comment")
.selectOne( .sendQuery(
"reference" to comment.target.reference, "reference" to comment.target.reference,
"resource" to comment "resource" to comment
)!! )
}
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>): CommentForView<TargetRef, CitizenCreator> { fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>) {
return requester requester
.getFunction("edit_comment") .getFunction("edit_comment")
.selectOne( .sendQuery(
"id" to comment.id, "id" to comment.id,
"content" to comment.content "content" to comment.content
)!! )
} }
} }
class CommentRepository(requester: Requester) : CommentRepositoryAbs<TargetRef>(requester) { 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") .getFunction("find_comment_by_id")
.selectOne<CommentForView<TargetRef, CitizenCreator>>(mapOf("id" to id)) .selectOne<CommentForView<TargetRef, CitizenCreator>>(mapOf("id" to id))
as CommentForView<TargetRef, CitizenCreatorI>? as CommentForView<TargetRef, CitizenCreatorI>?
}
override fun findByCitizen( override fun findByCitizen(
citizen: CitizenI, citizen: CitizenI,
page: Int, page: Int,
limit: Int limit: Int
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> = requester ): Paginated<CommentForView<TargetRef, CitizenCreatorI>> {
.getFunction("find_comments_by_citizen") return requester.run {
getFunction("find_comments_by_citizen")
.select<CommentForView<TargetRef, CitizenCreator>>( .select<CommentForView<TargetRef, CitizenCreator>>(
page, page,
limit, limit,
"created_by_id" to citizen.id "created_by_id" to citizen.id
) as Paginated<CommentForView<TargetRef, CitizenCreatorI>> ) as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
}
}
override fun findByParent( override fun findByParent(
parentId: UUID, parentId: UUID,
page: Int, page: Int,
limit: Int limit: Int
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> = requester ): Paginated<CommentForView<TargetRef, CitizenCreatorI>> {
.getFunction("find_comments_by_parent") return requester.run {
getFunction("find_comments_by_parent")
.select<CommentForView<TargetRef, CitizenCreator>>( .select<CommentForView<TargetRef, CitizenCreator>>(
page, page,
limit, limit,
@@ -110,3 +128,5 @@ class CommentRepository(requester: Requester) : CommentRepositoryAbs<TargetRef>(
) )
as Paginated<CommentForView<TargetRef, CitizenCreatorI>> 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 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.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl 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.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput 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.application.call
import io.ktor.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -28,40 +24,22 @@ object EditComment {
@Location("/comments/{comment}") @Location("/comments/{comment}")
class EditCommentRequest(comment: UUID) { class EditCommentRequest(comment: UUID) {
val comment = CommentRef(comment) val comment = CommentRef(comment)
class Input(val content: String) { class Input(val content: String)
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
} }
fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) { fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) {
put<EditCommentRequest> { put<EditCommentRequest> {
mustBeAuth() mustBeAuth()
val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.canUpdate(commentOld, citizenOrNull).assert() 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( call.respond(
HttpStatusCode.OK, 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.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.comment.generic.CommentAccessControl 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.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import fr.dcproject.routes.PaginatedRequest import fr.dcproject.routes.PaginatedRequest
@@ -22,13 +21,11 @@ import java.util.UUID
object GetCommentChildren { object GetCommentChildren {
@Location("/comments/{comment}/children") @Location("/comments/{comment}/children")
class CommentChildrenRequest( class CommentChildrenRequest(
comment: UUID, val comment: UUID,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val search: String? = null val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) : PaginatedRequestI by PaginatedRequest(page, limit)
val comment = CommentRef(comment)
}
fun Route.getChildrenComments(repo: CommentRepository, ac: CommentAccessControl) { fun Route.getChildrenComments(repo: CommentRepository, ac: CommentAccessControl) {
get<CommentChildrenRequest> { get<CommentChildrenRequest> {
@@ -39,7 +36,7 @@ object GetCommentChildren {
it.limit it.limit
) )
ac.canView(comments.result, citizenOrNull).assert() ac.assert { canView(comments.result, citizenOrNull) }
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,

View File

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

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.comment.generic.routes 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.EditComment.editComment
import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments
import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment

View File

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

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.constitution.routes package fr.dcproject.component.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull 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.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI 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.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -32,29 +27,12 @@ object FindConstitutions {
val sort: String? = null, val sort: String? = null,
val direction: RepositoryI.Direction? = null, val direction: RepositoryI.Direction? = null,
val search: String? = null val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) : 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)
}
fun Route.findConstitutions(repo: ConstitutionRepository, ac: ConstitutionAccessControl) { fun Route.findConstitutions(repo: ConstitutionRepository, ac: ConstitutionAccessControl) {
get<FindConstitutionsRequest> { get<FindConstitutionsRequest> {
it.validate().badRequestIfNotValid()
val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search) 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( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,
constitutions.toOutput { c -> constitutions.toOutput { c ->

View File

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

View File

@@ -7,8 +7,10 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get
import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI
fun Route.definition() { fun Route.definition() {
get("/") { get("/") {
call.respondText("/openapi.yaml".readResource(), ContentType("text", "yaml")) 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.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@KtorExperimentalAPI
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Routing.installDocRoutes() { fun Routing.installDocRoutes() {

View File

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

View File

@@ -8,18 +8,19 @@ import fr.dcproject.common.entity.HasTarget
import fr.dcproject.common.entity.TargetI import fr.dcproject.common.entity.TargetI
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import java.util.UUID import java.util.UUID
data class FollowForView<T : TargetI>( open class FollowForView<T : TargetI>(
override val id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val createdBy: CitizenCreator, override val createdBy: CitizenCreator,
override var target: T override var target: T
) : ExtraI<T, CitizenI>, ) : ExtraI<T, CitizenRef>,
FollowRef(id), FollowRef(id),
Created<CitizenI> by Created.Imp(createdBy) Created<CitizenRef> by Created.Imp(createdBy)
data class FollowForUpdate<T : TargetI, C : CitizenI>( class FollowForUpdate<T : TargetI, C : CitizenI>(
override val id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val target: T, override val target: T,
override val createdBy: C override val createdBy: C
) : FollowRef(id), ) : FollowRef(id),
@@ -30,4 +31,4 @@ open class FollowRef(
override val id: UUID override val id: UUID
) : FollowI ) : 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.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRef 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.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.constitution.database.ConstitutionForView import fr.dcproject.component.constitution.database.ConstitutionForView
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
@@ -74,24 +72,21 @@ sealed class FollowRepository<IN : TargetRef, OUT : TargetRef>(override var requ
target: Entity, target: Entity,
bulkSize: Int = 300 bulkSize: Int = 300
): Flow<FollowForView<IN>> = flow { ): Flow<FollowForView<IN>> = flow {
var lastId: UUID? = null var nextPage = 1
while (true) { do {
val result = findFollowsByTarget(target, lastId, bulkSize) val paginate = findFollowsByTarget(target, nextPage, bulkSize)
if (result.count() == 0) { paginate.result.forEach {
break
}
result.forEach {
emit(it) emit(it)
} }
lastId = result.last().id nextPage = paginate.currentPage + 1
} } while (!paginate.isLastPage())
} }
abstract fun findFollowsByTarget( abstract fun findFollowsByTarget(
target: Entity, target: Entity,
lastId: UUID?, page: Int = 1,
limit: Int = 300 limit: Int = 300
): List<FollowForView<IN>> ): Paginated<FollowForView<IN>>
} }
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) { class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
@@ -112,14 +107,14 @@ class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRe
override fun findFollowsByTarget( override fun findFollowsByTarget(
target: Entity, target: Entity,
lastId: UUID?, page: Int,
limit: Int limit: Int
): List<FollowForView<ArticleRef>> { ): Paginated<FollowForView<ArticleRef>> {
return requester return requester
.getFunction("find_follows_article_by_target") .getFunction("find_follows_article_by_target")
.select( .select(
"start_id" to lastId, page,
"limit" to limit, limit,
"target_id" to target.id "target_id" to target.id
) )
} }
@@ -143,34 +138,9 @@ class FollowConstitutionRepository(requester: Requester) : FollowRepository<Cons
override fun findFollowsByTarget( override fun findFollowsByTarget(
target: Entity, 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, page: Int,
limit: Int limit: Int
): Paginated<FollowForView<Citizen>> { ): Paginated<FollowForView<ConstitutionRef>> {
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>> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View File

@@ -28,7 +28,7 @@ object FollowArticle {
post<ArticleFollowRequest> { post<ArticleFollowRequest> {
mustBeAuth() mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen) val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.canCreate(follow, citizenOrNull).assert() ac.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow) repo.follow(follow)
call.respond(HttpStatusCode.Created) 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.auth.citizenOrNull
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.routes.citizen.toOutput
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -15,6 +14,7 @@ import io.ktor.locations.Location
import io.ktor.locations.get import io.ktor.locations.get
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@@ -27,10 +27,22 @@ object GetFollowArticle {
fun Route.getFollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) { fun Route.getFollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
get<ArticleFollowRequest> { get<ArticleFollowRequest> {
repo.findFollow(citizen, it.article)?.let { follow -> repo.findFollow(citizen, it.article)?.let { follow ->
ac.canView(follow, citizenOrNull).assert() ac.assert { canView(follow, citizenOrNull) }
call.respond( call.respond(
HttpStatusCode.OK, 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) } ?: call.respond(HttpStatusCode.NoContent)
} }

View File

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

View File

@@ -28,7 +28,7 @@ object UnfollowArticle {
delete<ArticleFollowRequest> { delete<ArticleFollowRequest> {
mustBeAuth() mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen) val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.canDelete(follow, citizenOrNull).assert() ac.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow) repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent) 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> { post<ConstitutionFollowRequest> {
mustBeAuth() mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen) val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.canCreate(follow, citizenOrNull).assert() ac.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow) repo.follow(follow)
call.respond(HttpStatusCode.Created) 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.constitution.database.ConstitutionRef
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.routes.citizen.toOutput
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -15,6 +14,7 @@ import io.ktor.locations.Location
import io.ktor.locations.get import io.ktor.locations.get
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@@ -27,10 +27,22 @@ object GetFollowConstitution {
fun Route.getFollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) { fun Route.getFollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
get<ConstitutionFollowRequest> { get<ConstitutionFollowRequest> {
repo.findFollow(citizen, it.constitution)?.let { follow -> repo.findFollow(citizen, it.constitution)?.let { follow ->
ac.canView(follow, citizenOrNull).assert() ac.assert { canView(follow, citizenOrNull) }
call.respond( call.respond(
HttpStatusCode.OK, 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) } ?: call.respond(HttpStatusCode.NotFound)
} }

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
package fr.dcproject.component.notification 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.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
@@ -14,11 +12,7 @@ import fr.dcproject.component.article.database.ArticleForView
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) open class Notification(
@JsonSubTypes(
JsonSubTypes.Type(value = ArticleUpdateNotificationMessage::class, name = "article")
)
open class NotificationMessage(
val type: String, val type: String,
val createdAt: DateTime = DateTime.now() val createdAt: DateTime = DateTime.now()
) { ) {
@@ -48,16 +42,16 @@ open class NotificationMessage(
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 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 class EntityNotification(
open val target: E, val target: Entity,
type: String, type: String,
val action: String val action: String
) : NotificationMessage(type) ) : Notification(type)
data class ArticleUpdateNotificationMessage( class ArticleUpdateNotification(
override val target: ArticleForView target: ArticleForView
) : EntityNotificationMessage<ArticleForView>(target, "article", "update") ) : 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 com.fasterxml.jackson.core.JsonProcessingException
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.citizen.database.CitizenI 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
import io.ktor.http.cio.websocket.Frame.Text import io.ktor.http.cio.websocket.Frame.Text
import io.ktor.http.cio.websocket.readText import io.ktor.http.cio.websocket.readText
@@ -29,42 +28,31 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/** class NotificationsPush private constructor(
* 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(
private val redis: RedisAsyncCommands<String, String>, private val redis: RedisAsyncCommands<String, String>,
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>, private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
citizen: CitizenI, citizen: CitizenI,
incoming: Flow<NotificationMessage>, incoming: Flow<Notification>,
onReceive: suspend (NotificationMessage) -> Unit, onRecieve: suspend (Notification) -> Unit,
) { ) {
class Builder(redisClient: RedisClient) { class Builder(val redisClient: RedisClient) {
private val redisConnection = redisClient.connect() private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redisConnectionPubSub = redisClient.connectPubSub() private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis PubSub")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() 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( fun build(
citizen: CitizenI, citizen: CitizenI,
incoming: Flow<NotificationMessage>, incoming: Flow<Notification>,
onReceive: suspend (NotificationMessage) -> Unit, onRecieve: suspend (Notification) -> Unit,
): NotificationPushListener = NotificationPushListener(redis, redisConnectionPubSub, citizen, incoming, onReceive) ): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve)
/**
* Build NotificationPush with only a WebSocket session
*/
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
fun build(ws: DefaultWebSocketServerSession): NotificationPushListener { fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
/* Convert channel of string from websocket, to a flow of Notification object */ /* Convert channel of string from websocket, to a flow of Notification object */
val incomingFlow: Flow<NotificationMessage> = ws.incoming.consumeAsFlow() val incomingFlow: Flow<Notification> = ws.incoming.consumeAsFlow()
.mapNotNull<Frame, Text> { it as? Text } .mapNotNull<Frame, Text> { it as? Frame.Text }
.map { it.readText() } .map { it.readText() }
.map { NotificationMessage.fromString(it) } .map { Notification.fromString(it) }
return build(ws.call.citizen, incomingFlow) { return build(ws.call.citizen, incomingFlow) {
ws.outgoing.send(Text(it.toString())) 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}" private val key = "notification:${citizen.id}"
/** private var score: Double = 0.0
* 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 val listener = object : RedisPubSubAdapter<String, String>() { private val listener = object : RedisPubSubAdapter<String, String>() {
/* On new key publish */ /* On new key publish */
override fun message(pattern: String?, channel: String?, message: String?) { override fun message(pattern: String?, channel: String?, message: String?) {
runBlocking { runBlocking {
getNewUnreadNotifications().collect { getNotifications().collect {
onReceive(it) onRecieve(it)
} }
} }
} }
} }
/**
* Init the listener and the callback
*/
init { init {
/* Mark as read all incoming notifications */ /* Mark as read all incoming notifications */
GlobalScope.launch { GlobalScope.launch {
incoming.collect { incoming.collect {
it.markAsRead() markAsRead(it)
} }
} }
/* Get old notification and sent it to websocket */ /* Get old notification and sent it to websocket */
runBlocking { runBlocking {
getNewUnreadNotifications().collect { getNotifications().collect { onRecieve(it) }
onReceive(it)
}
} }
/* Listen redis event, and sent the new notification into websocket */ /* Lisen redis event, and sent the new notification into websocket */
redisConnectionPubSub.run { redisConnectionPubSub.run {
addListener(listener) addListener(listener)
/* Register to the events */ /* 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() { fun close() {
redisConnectionPubSub.removeListener(listener) redisConnectionPubSub.removeListener(listener)
} }
/** /* Return flow with all new notifications */
* Get All new notification from redis and private fun getNotifications() = flow<Notification> {
* 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> {
redis redis
.zrangebyscoreWithScores( .zrangebyscoreWithScores(
key, key,
Range.from( Range.from(
Boundary.excluding(lastScore), Boundary.excluding(score),
Boundary.including(Double.POSITIVE_INFINITY) Boundary.including(Double.POSITIVE_INFINITY)
), ),
Limit.from(100) Limit.from(100)
) )
.get().forEach { .get().forEach {
/* Build message object from raw string and return it */ emit(Notification.fromString(it.value))
emit(NotificationMessage.fromString(it.value)) if (it.score > score) score = it.score
if (it.score > lastScore) lastScore = it.score
} }
} }
/** private suspend fun markAsRead(notificationMessage: Notification) = coroutineScope {
* Mark one notification as read.
*
* Internally, this method remove the message of the SortedSet in redis
*/
private suspend fun NotificationMessage.markAsRead() = coroutineScope {
try { try {
redis.zremrangebyscore( redis.zremrangebyscore(
key, key,
Range.from( Range.from(
Boundary.including(id), Boundary.including(notificationMessage.id),
Boundary.including(id) Boundary.including(notificationMessage.id)
) )
) )
} catch (e: JsonProcessingException) { } catch (e: JsonProcessingException) {

View File

@@ -7,15 +7,12 @@ import kotlinx.coroutines.coroutineScope
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
class NotificationPublisherAsync( class Publisher(
private val factory: ConnectionFactory, 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, private val exchangeName: String,
) { ) {
/** suspend fun <T : EntityNotification> publish(it: T): Deferred<Unit> = coroutineScope {
* Publish a new notification message to RabbitMQ
*/
suspend fun <T : EntityNotificationMessage<*>> publishAsync(it: T): Deferred<Unit> = coroutineScope {
async { async {
factory.newConnection().use { connection -> factory.newConnection().use { connection ->
connection.createChannel().use { channel -> 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 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.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.websocket.webSocket import io.ktor.websocket.webSocket
@@ -13,8 +13,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.notificationArticle(pushListenerBuilder: NotificationPushListener.Builder) { fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) {
webSocket("/notifications") { 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 fr.dcproject.component.citizen.database.CitizenRef
import java.util.UUID import java.util.UUID
data class Opinion<T : TargetI>( open class Opinion<T : TargetI>(
override val id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val createdBy: CitizenCreator, override val createdBy: CitizenCreator,
override val target: T, override val target: T,
val choice: OpinionChoice val choice: OpinionChoice
@@ -39,4 +39,4 @@ open class OpinionRef(
override val id: UUID override val id: UUID
) : OpinionI, TargetRef(id) ) : OpinionI, TargetRef(id)
sealed interface OpinionI : EntityI interface OpinionI : EntityI

View File

@@ -7,4 +7,6 @@ interface Opinionable {
val opinions: Opinions 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