1 Commits
master ... doc

Author SHA1 Message Date
4bb458e8d6 Add developer documentation fo create action 2021-04-09 00:20:58 +02:00
142 changed files with 1028 additions and 3923 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

@@ -87,7 +87,6 @@ class ArticleForListing(
ArticleRef(id), ArticleRef(id),
ArticleAuthI<CitizenCartI>, ArticleAuthI<CitizenCartI>,
Votable by VotableImp(), Votable by VotableImp(),
CreatedAt by CreatedAt.Imp(),
CreatedBy<CitizenCartI> CreatedBy<CitizenCartI>
interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> { interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {

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,69 +1,42 @@
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.assert { canView(result, citizenOrNull) } } .apply { ac.assert { canView(result, citizenOrNull) } }
.run { .run {

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,8 +43,6 @@ 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.assert { canView(result, citizenOrNull) } } .apply { ac.assert { canView(result, citizenOrNull) } }
.let { .let {
@@ -83,7 +51,6 @@ object FindArticles {
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

@@ -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
@@ -41,31 +35,11 @@ object UpsertArticle {
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,
@@ -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

@@ -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)
} }
} }
@@ -97,10 +62,7 @@ object Register {
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
@@ -96,10 +95,10 @@ open class CitizenRefWithUser(
open class CitizenRef( open class CitizenRef(
id: UUID = UUID.randomUUID() id: UUID = UUID.randomUUID()
) : TargetRef(id), ) : Entity(id),
CitizenI CitizenI
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,

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()
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
.apply { validate().badRequestIfNotValid() }
ac.assert { canChangePassword(it.citizen, citizenOrNull) } ac.assert { canChangePassword(it.citizen, citizenOrNull) }
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
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,28 +27,11 @@ 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.assert { canView(citizens.result, citizenOrNull) } ac.assert { canView(citizens.result, citizenOrNull) }
call.respond( call.respond(

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,36 +26,27 @@ 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() } CommentForUpdate(
.run { target = it.article,
CommentForUpdate( createdBy = citizen,
target = it.article, content = content
createdBy = citizen, )
content = content }.let { comment ->
) ac.assert { canCreate(comment, citizenOrNull) }
}.let { comment -> repo.comment(comment)
ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
call.respond( call.respond(
HttpStatusCode.Created, HttpStatusCode.Created,
comment.toOutput() comment.toOutput()
) )
} }
} }
} }
} }

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,31 +26,14 @@ 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.assert { canView(comments.result, citizenOrNull) } ac.assert { canView(comments.result, citizenOrNull) }

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,37 +26,27 @@ 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 {
CommentForUpdate(
target = it.constitution,
createdBy = citizen,
content = content
)
}.let { comment ->
ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
call.receiveOrBadRequest<Input>() call.respond(
.apply { validate().badRequestIfNotValid() } HttpStatusCode.Created,
.run { comment.toOutput()
CommentForUpdate( )
target = it.constitution, }
createdBy = citizen,
content = content
)
}.let { comment ->
ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
call.respond(
HttpStatusCode.Created,
comment.toOutput()
)
}
} }
} }
} }

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,36 +19,12 @@ 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.assert { canView(comments.result, citizenOrNull) } ac.assert { canView(comments.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -63,14 +63,12 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
constructor( constructor(
createdBy: C, createdBy: C,
parent: CommentParent<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(),
) )
} }

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>> {
@@ -48,7 +49,7 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
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>> { ): Paginated<CommentForView<T, CitizenCreatorI>> {
return findByTarget(target.id, page, limit, sort) return findByTarget(target.id, page, limit, sort)
} }
@@ -57,30 +58,36 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
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 {
.select<CommentForView<T, CitizenCreator>>( getFunction("find_comments_by_target")
page, .select<CommentForView<T, CitizenCreator>>(
limit, page,
"target_id" to targetId, limit,
"sort" to sort "target_id" to targetId,
) as Paginated<CommentForView<T, CitizenCreatorI>> "sort" to sort.sql
)
as Paginated<CommentForView<T, CitizenCreatorI>>
}
}
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>): CommentForView<TargetRef, CitizenCreator> = requester fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>) {
.getFunction("comment") requester
.selectOne( .getFunction("comment")
"reference" to comment.target.reference, .sendQuery(
"resource" to comment "reference" to comment.target.reference,
)!! "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
)!! )
} }
} }

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.assert { canCreate(newComment, citizenOrNull) }
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.assert { canUpdate(commentOld, citizenOrNull) } ac.assert { canUpdate(comment, citizenOrNull) }
call.receiveOrBadRequest<EditCommentRequest.Input>() comment.content = call.receiveOrBadRequest<EditCommentRequest.Input>().content
.apply { validate().badRequestIfNotValid() } repo.edit(comment)
.run {
CommentForUpdate( call.respond(
id = commentOld.id, HttpStatusCode.OK,
createdBy = commentOld.createdBy, comment.toOutput()
target = commentOld.target, )
parent = commentOld.parent,
content = content,
)
}
.let { repo.edit(it) }
.let {
call.respond(
HttpStatusCode.OK,
it.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> {

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,

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,27 +27,10 @@ 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.assert { canView(constitutions.result, citizenOrNull) } ac.assert { canView(constitutions.result, citizenOrNull) }
call.respond( call.respond(

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

@@ -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

@@ -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
@@ -30,7 +30,19 @@ object GetFollowArticle {
ac.assert { canView(follow, citizenOrNull) } 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

@@ -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.assert { canCreate(follow, citizenOrNull) }
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.assert { canView(follow, citizenOrNull) }
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.assert { canView(follows.result, citizenOrNull) }
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.assert { canDelete(follow, citizenOrNull) }
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

@@ -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
@@ -30,7 +30,19 @@ object GetFollowConstitution {
ac.assert { canView(follow, citizenOrNull) } 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

@@ -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(
val target: E, val target: Entity,
type: String, type: String,
val action: String val action: String
) : NotificationMessage(type) ) : Notification(type)
class ArticleUpdateNotificationMessage( class ArticleUpdateNotification(
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
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
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

@@ -1,6 +1,5 @@
package fr.dcproject.component.opinion.routes package fr.dcproject.component.opinion.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.entity.TargetRef import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
@@ -13,9 +12,6 @@ import fr.dcproject.component.opinion.database.Opinion
import fr.dcproject.routes.PaginatedRequest 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 io.konform.validation.Validation
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
@@ -38,22 +34,11 @@ object GetMyOpinionsArticle {
limit: Int = 50 limit: Int = 50
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) : PaginatedRequestI by PaginatedRequest(page, limit) {
val citizen = CitizenRef(citizen) val citizen = CitizenRef(citizen)
fun validate() = Validation<CitizenOpinionsArticleRequest> {
CitizenOpinionsArticleRequest::page {
minimum(1)
}
CitizenOpinionsArticleRequest::limit {
minimum(1)
maximum(50)
}
}.validate(this)
} }
fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) { fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
get<CitizenOpinionsArticleRequest> { get<CitizenOpinionsArticleRequest> {
mustBeAuth() mustBeAuth()
it.validate().badRequestIfNotValid()
val opinions: Paginated<Opinion<TargetRef>> = repo.findCitizenOpinions(citizen, it.page, it.limit) val opinions: Paginated<Opinion<TargetRef>> = repo.findCitizenOpinions(citizen, it.page, it.limit)
ac.assert { canView(opinions.result, citizenOrNull) } ac.assert { canView(opinions.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.vote.routes package fr.dcproject.component.vote.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
@@ -10,9 +9,6 @@ import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteArticleRepository import fr.dcproject.component.vote.database.VoteArticleRepository
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.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,22 +28,11 @@ object GetCitizenVotesOnArticle {
val search: String? = null val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) : PaginatedRequestI by PaginatedRequest(page, limit) {
val citizen = CitizenRef(citizen) val citizen = CitizenRef(citizen)
fun validate() = Validation<CitizenVoteArticleRequest> {
CitizenVoteArticleRequest::page {
minimum(1)
}
CitizenVoteArticleRequest::limit {
minimum(1)
maximum(50)
}
}.validate(this)
} }
fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) { fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) {
get<CitizenVoteArticleRequest> { get<CitizenVoteArticleRequest> {
mustBeAuth() mustBeAuth()
it.validate().badRequestIfNotValid()
val votes = repo.findByCitizen(it.citizen, it.page, it.limit) val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
ac.assert { canView(votes.result, citizenOrNull) } ac.assert { canView(votes.result, citizenOrNull) }

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.vote.routes package fr.dcproject.component.vote.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.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
@@ -11,9 +10,6 @@ import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteArticleRepository import fr.dcproject.component.vote.database.VoteArticleRepository
import fr.dcproject.component.vote.database.VoteForUpdate import fr.dcproject.component.vote.database.VoteForUpdate
import io.konform.validation.Validation
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.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -29,22 +25,13 @@ object PutVoteOnArticle {
@Location("/articles/{article}/vote") @Location("/articles/{article}/vote")
class ArticleVoteRequest(article: UUID) { class ArticleVoteRequest(article: UUID) {
val article = ArticleRef(article) val article = ArticleRef(article)
data class Input(var note: Int) { data class Input(var note: Int)
fun validate() = Validation<Input> {
Input::note {
minimum(-1)
maximum(1)
}
}.validate(this)
}
} }
fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) { fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) {
put<ArticleVoteRequest> { put<ArticleVoteRequest> {
mustBeAuth() mustBeAuth()
val input = call.receiveOrBadRequest<ArticleVoteRequest.Input>() val input = call.receiveOrBadRequest<ArticleVoteRequest.Input>()
.apply { validate().badRequestIfNotValid() }
val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found") val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
val vote = VoteForUpdate( val vote = VoteForUpdate(
target = article, target = article,

View File

@@ -1,21 +1,15 @@
package fr.dcproject.component.vote.routes package fr.dcproject.component.vote.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.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.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteCommentRepository import fr.dcproject.component.vote.database.VoteCommentRepository
import fr.dcproject.component.vote.database.VoteForUpdate import fr.dcproject.component.vote.database.VoteForUpdate
import io.konform.validation.Validation
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.features.NotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
@@ -27,29 +21,18 @@ import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object PutVoteOnComment { object PutVoteOnComment {
@Location("/comments/{comment}/vote") @Location("/comments/{comment}/vote")
class CommentVoteRequest(comment: UUID) { class CommentVoteRequest(val comment: UUID) {
val comment = CommentRef(comment) data class Content(var note: Int)
data class Input(var note: Int) {
fun validate() = Validation<Input> {
Input::note {
minimum(-1)
maximum(1)
}
}.validate(this)
}
} }
fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, ac: VoteAccessControl) { fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, ac: VoteAccessControl) {
put<CommentVoteRequest> { put<CommentVoteRequest> {
mustBeAuth() mustBeAuth()
val comment = commentRepo.findById(it.comment)!!
val comment = commentRepo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found") val content = call.receiveOrBadRequest<CommentVoteRequest.Content>()
val input = call.receiveOrBadRequest<CommentVoteRequest.Input>()
.apply { validate().badRequestIfNotValid() }
val vote = VoteForUpdate( val vote = VoteForUpdate(
target = comment, target = comment,
note = input.note, note = content.note,
createdBy = this.citizen createdBy = this.citizen
) )
ac.assert { canCreate(vote, citizenOrNull) } ac.assert { canCreate(vote, citizenOrNull) }

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.vote.routes package fr.dcproject.component.vote.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.auth.citizen import fr.dcproject.component.auth.citizen
@@ -12,9 +11,6 @@ import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteConstitutionRepository import fr.dcproject.component.vote.database.VoteConstitutionRepository
import fr.dcproject.component.vote.database.VoteForUpdate import fr.dcproject.component.vote.database.VoteForUpdate
import fr.dcproject.component.vote.routes.PutVoteOnConstitution.ConstitutionVoteRequest.Input import fr.dcproject.component.vote.routes.PutVoteOnConstitution.ConstitutionVoteRequest.Input
import io.konform.validation.Validation
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.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -30,25 +26,17 @@ object PutVoteOnConstitution {
@Location("/constitutions/{constitution}/vote") @Location("/constitutions/{constitution}/vote")
class ConstitutionVoteRequest(constitution: UUID) { class ConstitutionVoteRequest(constitution: UUID) {
val constitution = ConstitutionRef(constitution) val constitution = ConstitutionRef(constitution)
data class Input(var note: Int) { data class Input(var note: Int)
fun validate() = Validation<Input> {
Input::note {
minimum(-1)
maximum(1)
}
}.validate(this)
}
} }
fun Route.voteConstitution(repo: VoteConstitutionRepository, ac: VoteAccessControl, constitutionRepo: ConstitutionRepository) { fun Route.voteConstitution(repo: VoteConstitutionRepository, ac: VoteAccessControl, constitutionRepo: ConstitutionRepository) {
put<ConstitutionVoteRequest> { put<ConstitutionVoteRequest> {
mustBeAuth() mustBeAuth()
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}")
val input = call.receiveOrBadRequest<Input>() val content = call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
val vote = VoteForUpdate( val vote = VoteForUpdate(
target = constitution, target = constitution,
note = input.note, note = content.note,
createdBy = this.citizen createdBy = this.citizen
) )
ac.assert { canCreate(vote, citizenOrNull) } ac.assert { canCreate(vote, citizenOrNull) }

View File

@@ -1,9 +1,8 @@
package fr.dcproject.component.workgroup.routes package fr.dcproject.component.workgroup.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.common.validation.isUrl
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
@@ -11,9 +10,6 @@ import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupForUpdate import fr.dcproject.component.workgroup.database.WorkgroupForUpdate
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
import fr.dcproject.component.workgroup.routes.CreateWorkgroup.PostWorkgroupRequest.Input import fr.dcproject.component.workgroup.routes.CreateWorkgroup.PostWorkgroupRequest.Input
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
@@ -33,30 +29,13 @@ object CreateWorkgroup {
val description: String, val description: String,
val logo: String?, val logo: String?,
val anonymous: Boolean? val anonymous: Boolean?
) { )
fun validate() = Validation<Input> {
Input::name {
minLength(5)
maxLength(80)
}
Input::description {
minLength(50)
maxLength(6000)
}
Input::logo ifPresent {
isUrl()
maxLength(2048)
}
}.validate(this)
}
} }
fun Route.createWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.createWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
post<PostWorkgroupRequest> { post<PostWorkgroupRequest> {
mustBeAuth() mustBeAuth()
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>().run {
validate().badRequestIfNotValid()
WorkgroupForUpdate( WorkgroupForUpdate(
id ?: UUID.randomUUID(), id ?: UUID.randomUUID(),
name, name,

View File

@@ -1,18 +1,13 @@
package fr.dcproject.component.workgroup.routes package fr.dcproject.component.workgroup.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.isUrl
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.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupForUpdate import fr.dcproject.component.workgroup.database.WorkgroupForUpdate
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
import fr.dcproject.component.workgroup.routes.EditWorkgroup.PutWorkgroupRequest.Input import fr.dcproject.component.workgroup.routes.EditWorkgroup.PutWorkgroupRequest.Input
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
@@ -20,33 +15,19 @@ import io.ktor.locations.Location
import io.ktor.locations.put import io.ktor.locations.put
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object EditWorkgroup { object EditWorkgroup {
@Location("/workgroups/{workgroupId}") @Location("/workgroups/{workgroupId}")
class PutWorkgroupRequest(val workgroupId: UUID) { class PutWorkgroupRequest(val workgroupId: UUID) : KoinComponent {
class Input( class Input(
val name: String?, val name: String?,
val description: String?, val description: String?,
val logo: String?, val logo: String?,
val anonymous: Boolean? val anonymous: Boolean?
) { )
fun validate() = Validation<Input> {
Input::name ifPresent {
minLength(5)
maxLength(80)
}
Input::description ifPresent {
minLength(50)
maxLength(6000)
}
Input::logo ifPresent {
isUrl()
maxLength(2048)
}
}.validate(this)
}
} }
fun Route.editWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.editWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
@@ -54,7 +35,6 @@ object EditWorkgroup {
mustBeAuth() mustBeAuth()
repo.findById(it.workgroupId)?.let { old -> repo.findById(it.workgroupId)?.let { old ->
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>().run {
validate().badRequestIfNotValid()
WorkgroupForUpdate( WorkgroupForUpdate(
id = old.id, id = old.id,
name = name ?: old.name, name = name ?: old.name,

View File

@@ -1,20 +1,12 @@
package fr.dcproject.component.workgroup.routes package fr.dcproject.component.workgroup.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.utils.toUUID
import fr.dcproject.common.validation.isUuid
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
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.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -35,40 +27,23 @@ object GetWorkgroups {
val search: String? = null, val search: String? = null,
val createdBy: String? = null, val createdBy: String? = null,
members: List<String?>? = null members: List<String?>? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
val members: List<UUID>? = members?.toUUID() val members: List<UUID>? = members?.toUUID()
fun validate() = Validation<WorkgroupsRequest> {
WorkgroupsRequest::page {
minimum(1)
}
WorkgroupsRequest::limit {
minimum(1)
maximum(50)
}
WorkgroupsRequest::sort ifPresent {
enum(
"name",
"createdAt",
)
}
WorkgroupsRequest::createdBy ifPresent {
isUuid()
}
}.validate(this)
} }
fun Route.getWorkgroups(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.getWorkgroups(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
get<WorkgroupsRequest> { get<WorkgroupsRequest> {
it.validate().badRequestIfNotValid() val workgroups =
repo.find(
val workgroups = repo.find( it.page,
it.page, it.limit,
it.limit, it.sort,
it.sort, it.direction,
it.direction, it.search,
it.search, WorkgroupRepository.Filter(createdById = it.createdBy, members = it.members)
WorkgroupRepository.Filter(createdById = it.createdBy, members = it.members) )
)
ac.assert { canView(workgroups.result, citizenOrNull) } ac.assert { canView(workgroups.result, citizenOrNull) }
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,

View File

@@ -17,12 +17,13 @@ import io.ktor.locations.Location
import io.ktor.locations.post import io.ktor.locations.post
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object AddMemberToWorkgroup { object AddMemberToWorkgroup {
@Location("/workgroups/{workgroupId}/members") @Location("/workgroups/{workgroupId}/members")
class WorkgroupsMembersRequest(val workgroupId: UUID) { class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
class Input : MutableList<Input.Member> by mutableListOf() { class Input : MutableList<Input.Member> by mutableListOf() {
class Member(val citizen: CitizenRef, roles: List<String> = emptyList()) { class Member(val citizen: CitizenRef, roles: List<String> = emptyList()) {
val roles: List<WorkgroupWithMembersI.Member.Role> = roles.map { val roles: List<WorkgroupWithMembersI.Member.Role> = roles.map {

View File

@@ -17,13 +17,14 @@ import io.ktor.locations.Location
import io.ktor.locations.delete import io.ktor.locations.delete
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID import java.util.UUID
import fr.dcproject.component.workgroup.routes.members.DeleteMembersOfWorkgroup.WorkgroupsMembersRequest.Input as Input import fr.dcproject.component.workgroup.routes.members.DeleteMembersOfWorkgroup.WorkgroupsMembersRequest.Input as Input
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object DeleteMembersOfWorkgroup { object DeleteMembersOfWorkgroup {
@Location("/workgroups/{workgroupId}/members") @Location("/workgroups/{workgroupId}/members")
class WorkgroupsMembersRequest(val workgroupId: UUID) { class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
class Input : MutableList<Input.Member> by mutableListOf() { class Input : MutableList<Input.Member> by mutableListOf() {
class Member(val citizen: CitizenRef) class Member(val citizen: CitizenRef)
} }

View File

@@ -17,12 +17,13 @@ import io.ktor.locations.Location
import io.ktor.locations.put import io.ktor.locations.put
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object UpdateMemberOfWorkgroup { object UpdateMemberOfWorkgroup {
@Location("/workgroups/{workgroupId}/members") @Location("/workgroups/{workgroupId}/members")
class WorkgroupsMembersRequest(val workgroupId: UUID) { class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
class Input : MutableList<Input.Item> by mutableListOf() { class Input : MutableList<Input.Item> by mutableListOf() {
class Item(val citizen: CitizenRef, roles: List<String> = emptyList()) { class Item(val citizen: CitizenRef, roles: List<String> = emptyList()) {
val roles: List<WorkgroupWithMembersI.Member.Role> = roles.map { val roles: List<WorkgroupWithMembersI.Member.Role> = roles.map {

View File

@@ -41,12 +41,6 @@ paths:
maxItems: 50 maxItems: 50
items: items:
$ref: '#/components/schemas/ArticleListingResponse' $ref: '#/components/schemas/ArticleListingResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [] - JWTAuth: []
@@ -71,21 +65,16 @@ paths:
Limit power of press Limit power of press
content: content:
type: string type: string
minLength: 50
maxLength: 6000
example: example:
Lorem upsum... Lorem upsum...
description: description:
type: string type: string
minLength: 50
maxLength: 6000
example: example:
I think is the bether choice I think is the bether choice
tags: tags:
type: array type: array
items: items:
type: string type: string
maxItems: 15
default: [ ] default: [ ]
example: [ power, press ] example: [ power, press ]
anonymous: anonymous:
@@ -117,12 +106,6 @@ paths:
format: uuid format: uuid
versionNumber: versionNumber:
type: integer type: integer
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
403: 403:
@@ -145,12 +128,6 @@ paths:
tags: tags:
- article - article
operationId: getArticle operationId: getArticle
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/sort'
- $ref: '#/components/parameters/direction'
- $ref: '#/components/parameters/search'
responses: responses:
200: 200:
description: The Article objects description: The Article objects
@@ -158,31 +135,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ArticleResponse' $ref: '#/components/schemas/ArticleResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
404:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/404'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/401'
403:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/403'
/articles/{article}/versions: /articles/{article}/versions:
parameters: parameters:
- $ref: '#/components/parameters/article' - $ref: '#/components/parameters/article'
@@ -191,12 +143,6 @@ paths:
tags: tags:
- article - article
operationId: getArticleVersions operationId: getArticleVersions
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/sort'
- $ref: '#/components/parameters/direction'
- $ref: '#/components/parameters/search'
responses: responses:
200: 200:
description: The versions of Article description: The versions of Article
@@ -247,12 +193,6 @@ paths:
format: uuid format: uuid
name: name:
type: string type: string
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/login: /login:
post: post:
@@ -370,7 +310,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/400' description: sdf
/auth/passwordless: /auth/passwordless:
post: post:
summary: Send a connexion link by email summary: Send a connexion link by email
@@ -414,7 +354,7 @@ paths:
parameters: parameters:
- $ref: '#/components/parameters/page' - $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit' - $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/citizenSort' - $ref: '#/components/parameters/sort'
- $ref: '#/components/parameters/direction' - $ref: '#/components/parameters/direction'
- $ref: '#/components/parameters/search' - $ref: '#/components/parameters/search'
responses: responses:
@@ -431,12 +371,6 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/CitizenListResponse' $ref: '#/components/schemas/CitizenListResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/citizens/current: /citizens/current:
@@ -509,10 +443,6 @@ paths:
description: Password changed description: Password changed
400: 400:
description: Bad request description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
404: 404:
@@ -534,13 +464,13 @@ paths:
in: query in: query
required: false required: false
example: example:
- createdAt - created_at
- votes - votes
schema: schema:
type: string type: string
default: createdAt default: created_at
enum: enum:
- createdAt - created_at
- votes - votes
responses: responses:
200: 200:
@@ -556,12 +486,6 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [ ] - JWTAuth: [ ]
@@ -579,10 +503,8 @@ paths:
properties: properties:
content: content:
type: string type: string
minLength: 20
maxLength: 6000
example: example:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum... Lorem ipsum...
responses: responses:
201: 201:
description: Return created Comment description: Return created Comment
@@ -590,12 +512,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/comments/{comment}: /comments/{comment}:
@@ -612,42 +528,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
post:
security:
- JWTAuth: []
summary: create comment
tags:
- comment
requestBody:
content:
application/json:
schema:
required:
- content
properties:
content:
type: string
minLength: 20
maxLength: 6000
example:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
responses:
201:
description: Return updated comment
content:
application/json:
schema:
$ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401:
$ref: '#/components/responses/401'
404:
description: No comment found
put: put:
security: security:
- JWTAuth: [] - JWTAuth: []
@@ -663,10 +543,8 @@ paths:
properties: properties:
content: content:
type: string type: string
minLength: 20
maxLength: 6000
example: example:
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum...
responses: responses:
200: 200:
description: Return updated comment description: Return updated comment
@@ -674,12 +552,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/comments/{comment}/children: /comments/{comment}/children:
@@ -765,42 +637,13 @@ paths:
tags: tags:
- comment - comment
- constitution - constitution
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/search'
- name: sort
in: query
required: false
example:
- createdAt
- votes
schema:
type: string
default: createdAt
enum:
- createdAt
- votes
responses: responses:
200: 200:
description: Return Comment and children description: Return Comment and children
content: content:
application/json: application/json:
schema: schema:
allOf: $ref: '#/components/schemas/CommentResponse'
- $ref: '#/components/schemas/Paginated'
- type: object
properties:
result:
type: array
items:
$ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [] - JWTAuth: []
@@ -817,10 +660,8 @@ paths:
properties: properties:
content: content:
type: string type: string
minLength: 20
maxLength: 6000
example: example:
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum...
responses: responses:
201: 201:
description: Return created comment description: Return created comment
@@ -828,12 +669,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
@@ -863,12 +698,6 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/ConstitutionListingResponse' $ref: '#/components/schemas/ConstitutionListingResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [ ] - JWTAuth: [ ]
@@ -893,11 +722,7 @@ paths:
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
400: 400:
description: BadReqest $ref: '#/components/responses/400'
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/constitutions/{constitution}: /constitutions/{constitution}:
parameters: parameters:
- $ref: '#/components/parameters/constitution' - $ref: '#/components/parameters/constitution'
@@ -955,105 +780,13 @@ paths:
description: Return only http status 204 on success description: Return only http status 204 on success
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/citizens/{citizen}/follows:
parameters:
- $ref: '#/components/parameters/citizen'
get:
security:
- JWTAuth: [ ]
summary: Return Follows of citizen
tags:
- follow
- citizen
responses:
200:
description: Return follows
content:
application/json:
schema:
$ref: '#/components/schemas/FollowResponse'
404:
description: Citizen not exist
content:
application/json:
schema:
$ref: '#/components/schemas/404'
post:
security:
- JWTAuth: []
summary: Follow citizen
description: Follow a citizen to receive notifications of his activity
tags:
- follow
- citizen
responses:
201:
description: Return only http status 201 on success
401:
$ref: '#/components/responses/401'
404:
description: Citizen not exist
content:
application/json:
schema:
$ref: '#/components/schemas/404'
delete:
security:
- JWTAuth: [ ]
summary: Unfollow one citizen
tags:
- follow
- citizen
responses:
204:
description: Return only http status 204 on success
401:
$ref: '#/components/responses/401'
404:
description: Citizen not exist
content:
application/json:
schema:
$ref: '#/components/schemas/404'
/citizens/{citizen}/follows/citizens:
parameters:
- $ref: '#/components/parameters/citizen'
get:
security:
- JWTAuth: [ ]
summary: Return citizen Follow of citizen
tags:
- follow
- citizen
responses:
200:
description: Return citizen Follow of citizen
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Paginated'
- type: object
properties:
result:
type: array
items:
$ref: '#/components/schemas/FollowResponse'
404:
description: Citizen not exist
content:
application/json:
schema:
$ref: '#/components/schemas/404'
/citizens/{citizen}/follows/articles: /citizens/{citizen}/follows/articles:
parameters: parameters:
- $ref: '#/components/parameters/citizen' - $ref: '#/components/parameters/citizen'
get: get:
security: security:
- JWTAuth: [ ] - JWTAuth: [ ]
summary: Return article Follow of citizen summary: Return Follow or nothing if you not follow
tags: tags:
- follow - follow
- article - article
@@ -1128,7 +861,7 @@ paths:
- citizen - citizen
responses: responses:
200: 200:
description: Return constitution Follow of citizen description: Return your follows
content: content:
application/json: application/json:
schema: schema:
@@ -1222,9 +955,6 @@ paths:
tags: tags:
- opinion - opinion
- citizen - citizen
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
responses: responses:
200: 200:
description: Opinions description: Opinions
@@ -1239,13 +969,6 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/Opinion' $ref: '#/components/schemas/Opinion'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/articles/{article}/opinions: /articles/{article}/opinions:
parameters: parameters:
- $ref: '#/components/parameters/article' - $ref: '#/components/parameters/article'
@@ -1298,12 +1021,6 @@ paths:
responses: responses:
201: 201:
description: Return only http status 201 on success description: Return only http status 201 on success
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/citizens/{citizen}/votes: /citizens/{citizen}/votes:
parameters: parameters:
- $ref: '#/components/parameters/citizen' - $ref: '#/components/parameters/citizen'
@@ -1362,12 +1079,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/VoteAggregation' $ref: '#/components/schemas/VoteAggregation'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/citizens/{citizen}/votes/articles: /citizens/{citizen}/votes/articles:
@@ -1381,9 +1092,6 @@ paths:
- vote - vote
- article - article
- citizen - citizen
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
responses: responses:
200: 200:
description: Votes description: Votes
@@ -1398,12 +1106,6 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/VoteResponse' $ref: '#/components/schemas/VoteResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/articles/{article}/vote: /articles/{article}/vote:
@@ -1428,12 +1130,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/VoteAggregation' $ref: '#/components/schemas/VoteAggregation'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
@@ -1463,12 +1159,6 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/WorkgroupListing' $ref: '#/components/schemas/WorkgroupListing'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
summary: Create new Workgroup summary: Create new Workgroup
security: security:
@@ -1507,12 +1197,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Workgroup' $ref: '#/components/schemas/Workgroup'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/workgroups/{workgroup}: /workgroups/{workgroup}:
parameters: parameters:
- $ref: '#/components/parameters/workgroup' - $ref: '#/components/parameters/workgroup'
@@ -1561,12 +1245,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Workgroup' $ref: '#/components/schemas/Workgroup'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
delete: delete:
summary: Delete one workgroup summary: Delete one workgroup
security: security:
@@ -1695,17 +1373,6 @@ components:
- createdAt - createdAt
- vote - vote
- popularity - popularity
citizenSort:
name: sort
in: query
description: The sort field name
example: createdAt
required: false
schema:
type: string
enum:
- title
- createdAt
workgroupSort: workgroupSort:
name: sort name: sort
in: query in: query
@@ -2188,8 +1855,6 @@ components:
$ref: '#/components/schemas/UUID' $ref: '#/components/schemas/UUID'
title: title:
type: string type: string
minLength: 10
maxLength: 80
example: example:
Constitution for the liberty Constitution for the liberty
titles: titles:
@@ -2205,8 +1870,6 @@ components:
$ref: '#/components/schemas/UUID' $ref: '#/components/schemas/UUID'
name: name:
type: string type: string
minLength: 10
maxLength: 80
example: example:
The liberties The liberties
articles: articles:
@@ -2546,60 +2209,6 @@ components:
- REPORTER - REPORTER
example: MASTER example: MASTER
400:
description: Bad Request
required:
- title
- invalidParams
additionalProperties: false
properties:
statusCode:
type: integer
example: 400
title:
type: string
example: Bad Request
invalidParams:
type: array
items:
required:
- name
- reason
properties:
name:
type: string
example: '.title'
reason:
type: string
example: 'Cannot be null'
403:
description: Forbiden
properties:
statusCode:
type: integer
title:
type: string
401:
description: Unauthorized
properties:
statusCode:
type: integer
title:
type: string
404:
description: Not Found
required:
- title
- statusCode
additionalProperties: false
properties:
statusCode:
type: integer
example: 404
title:
type: string
example: Bad Request
securitySchemes: securitySchemes:
JWTAuth: JWTAuth:
type: http type: http

View File

@@ -23,7 +23,6 @@ begin
select select
a.id, a.id,
a.title, a.title,
a.created_at,
a.deleted_at, a.deleted_at,
a.draft, a.draft,
find_citizen_by_id_with_user(a.created_by_id) as created_by, find_citizen_by_id_with_user(a.created_by_id) as created_by,
@@ -46,7 +45,7 @@ begin
case direction when 'asc' then case direction when 'asc' then
case sort case sort
when 'title' then a.title when 'title' then a.title
when 'createdAt' then a.created_at::text when 'created_at' then a.created_at::text
when 'vote' then ca.score::text when 'vote' then ca.score::text
when 'popularity' then ca.total::text when 'popularity' then ca.total::text
else null else null
@@ -55,7 +54,7 @@ begin
case direction when 'desc' then case direction when 'desc' then
case sort case sort
when 'title' then a.title when 'title' then a.title
when 'createdAt' then a.created_at::text when 'created_at' then a.created_at::text
when 'vote' then ca.score::text when 'vote' then ca.score::text
when 'popularity' then ca.total::text when 'popularity' then ca.total::text
end end

View File

@@ -23,14 +23,14 @@ begin
case direction when 'asc' then case direction when 'asc' then
case sort case sort
when 'name' then (z.name->'first_name')::text when 'name' then (z.name->'first_name')::text
when 'createdAt' then z.created_at::text when 'created_at' then z.created_at::text
else null else null
end end
end, end,
case direction when 'desc' then case direction when 'desc' then
case sort case sort
when 'name' then (z.name->'first_name')::text when 'name' then (z.name->'first_name')::text
when 'createdAt' then z.created_at::text when 'created_at' then z.created_at::text
end end
end end
desc, desc,

View File

@@ -1,4 +1,4 @@
create or replace function comment(reference regclass, inout resource json) create or replace function comment(reference regclass, resource json, out _id uuid)
language plpgsql as language plpgsql as
$$ $$
declare declare
@@ -17,8 +17,7 @@ begin
else else
raise exception 'comment with target as "%", is not implemented', reference::text; raise exception 'comment with target as "%", is not implemented', reference::text;
end if; end if;
_id = _new_id;
select find_comment_by_id(_new_id) into resource;
end; end;
$$; $$;

View File

@@ -1,11 +1,9 @@
create or replace function edit_comment(_id uuid, _content text, out resource json) create or replace function edit_comment(_id uuid, _content text) returns void
language plpgsql as language plpgsql as
$$ $$
begin begin
update comment c set update comment c set
"content" = _content "content" = _content
where c.id = _id; where c.id = _id;
select find_comment_by_id(_id) into resource;
end; end;
$$; $$;

View File

@@ -26,7 +26,7 @@ begin
else null else null
end desc, end desc,
case sort case sort
when 'createdAt' then com.created_at::text when 'created_at' then com.created_at::text
else null else null
end desc, end desc,
com.created_at desc com.created_at desc

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