Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03350db56f | |||
| f380231e1e | |||
| 1c013e3e15 | |||
| 003aa9ff05 | |||
| 44c5f1b15b | |||
| 2f3a2f9b8e | |||
| 1a62c5ec9a | |||
| 55674cbef1 | |||
| ac5dbbc384 | |||
| 9460992f37 | |||
| 8013cfb266 | |||
| f577ea70b3 | |||
| 2673cf527a | |||
| 371483ccde | |||
| 76e4033a22 | |||
| fee5e5784b | |||
| 1c33c026f0 | |||
| 4871e7d780 | |||
| 359450ad8f | |||
| 11903a4cda | |||
| 1a8b544cdb | |||
| e2c1f15ab8 | |||
| dc87c95bb4 | |||
| d4cc3f21da | |||
| 59c050d14d | |||
| cb9bd0c14b | |||
| 1ce3a18d8e | |||
| be6fb8dc12 | |||
| cd9fd569d7 | |||
| 2920186352 | |||
| cccabb2cc9 | |||
| 620cd73fec | |||
| e474a40068 | |||
| 242bf9c9b3 | |||
| 543f3fb9bb | |||
| 994e266b52 | |||
| 3f392eece6 | |||
| 518b59e9aa | |||
| 596b7ff0c9 | |||
| 87175eb8ea | |||
| 1118866856 | |||
| 367f59ee18 | |||
| 0588f88f9a | |||
| 496cf50d88 | |||
| 13253e4af1 | |||
| 39c665b7a9 | |||
| 50b4cf1816 | |||
| 6a5e00bb4d | |||
| 0c8bcbd634 | |||
| 8223dd21bb | |||
| 27e405c585 | |||
| 34513e25b6 | |||
| f5c1aa29e8 | |||
| 875d0bfffa | |||
| fb7b07340a | |||
| a07b19a3cb | |||
| 13cdaaf01a | |||
| e473e62068 | |||
| 9d3eeeb04b | |||
| eb399392c9 | |||
| 1ec1c59c8c | |||
| 9511331cd2 | |||
| 33a8cdb169 | |||
| 6aa3ddb28d | |||
| 708d241a26 | |||
| e4745e71c2 | |||
| e26710898e | |||
| fe11384ad2 | |||
| 61a7091736 | |||
| 2ef9f65f2c | |||
| b5fc3d25bb | |||
| 3faf2e5f0d | |||
| ab418ae300 | |||
| 395d64a44a | |||
| a300e275d4 | |||
| 3a18ef0554 |
76
.github/workflows/tests.yml
vendored
76
.github/workflows/tests.yml
vendored
@@ -4,6 +4,9 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
@@ -18,6 +21,10 @@ 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
|
||||||
@@ -29,26 +36,17 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-build-action@v2
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.8
|
gradle-version: '7.4'
|
||||||
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
|
||||||
uses: eskatos/gradle-command-action@v1
|
run: gradle processResources
|
||||||
with:
|
|
||||||
gradle-version: 6.8
|
|
||||||
arguments: processResources
|
|
||||||
- name: processTestResources
|
- name: processTestResources
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-build-action@v2
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.8
|
gradle-version: '7.4'
|
||||||
arguments: processResources
|
arguments: processResources
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
@@ -69,10 +67,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: Build
|
name: Build
|
||||||
path: build
|
path: build
|
||||||
- name: TestSql
|
|
||||||
uses: eskatos/gradle-command-action@v1
|
- name: Composer Up
|
||||||
|
uses: gradle/gradle-build-action@v2
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.8
|
gradle-version: '7.4'
|
||||||
|
arguments: testSqlComposeUp
|
||||||
|
|
||||||
|
- name: TestSql
|
||||||
|
uses: gradle/gradle-build-action@v2
|
||||||
|
with:
|
||||||
|
gradle-version: '7.4'
|
||||||
arguments: testSql
|
arguments: testSql
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@@ -81,37 +86,58 @@ 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: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-build-action@v2
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.8
|
gradle-version: '7.4'
|
||||||
arguments: test -x testSql
|
arguments: test
|
||||||
|
|
||||||
- name: Coverage
|
- name: Coverage
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-build-action@v2
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.8
|
gradle-version: '7.4'
|
||||||
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 }}
|
||||||
run: ./gradlew build sonarqube --info
|
with:
|
||||||
|
gradle-version: '7.4'
|
||||||
|
arguments: sonarqube --info
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
needs: build
|
needs: build
|
||||||
@@ -128,7 +154,7 @@ jobs:
|
|||||||
name: Build
|
name: Build
|
||||||
path: build
|
path: build
|
||||||
- name: Lint
|
- name: Lint
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-build-action@v2
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.8
|
gradle-version: '7.4'
|
||||||
arguments: ktlintCheck
|
arguments: ktlintCheck
|
||||||
|
|||||||
12
.idea/codeStyles/Project.xml
generated
12
.idea/codeStyles/Project.xml
generated
@@ -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="5">
|
<SqlCodeStyleSettings version="6">
|
||||||
<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,21 +56,13 @@
|
|||||||
</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>
|
||||||
|
|||||||
@@ -25,12 +25,9 @@
|
|||||||
<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" />
|
||||||
|
|||||||
2
.idea/runConfigurations/Build_without_test.xml
generated
2
.idea/runConfigurations/Build_without_test.xml
generated
@@ -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" />
|
<option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt" />
|
||||||
<option name="taskDescriptions">
|
<option name="taskDescriptions">
|
||||||
<list />
|
<list />
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
23
.idea/runConfigurations/Create_lock_dependencies.xml
generated
Normal file
23
.idea/runConfigurations/Create_lock_dependencies.xml
generated
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
4
.idea/runConfigurations/Run_dependencies.xml
generated
4
.idea/runConfigurations/Run_dependencies.xml
generated
@@ -24,13 +24,9 @@
|
|||||||
<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" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Sonarqube without test" type="GradleRunConfiguration" factoryName="Gradle">
|
<configuration default="false" name="Sonarqube (Send without run 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$" />
|
||||||
23
.idea/runConfigurations/Test_With_Dependencies.xml
generated
Normal file
23
.idea/runConfigurations/Test_With_Dependencies.xml
generated
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
23
.idea/runConfigurations/Update_Dependency.xml
generated
Normal file
23
.idea/runConfigurations/Update_Dependency.xml
generated
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
# DC Project
|
# DC Project
|
||||||
|
|
||||||
[](https://www.codefactor.io/repository/github/flecomte/dc-project)
|
|
||||||
[](https://www.codacy.com/gh/flecomte/dc-project/dashboard?utm_source=github.com&utm_medium=referral&utm_content=flecomte/dc-project&utm_campaign=Badge_Grade)
|
|
||||||
[](https://sonarcloud.io/dashboard?id=dc-project)
|
[](https://sonarcloud.io/dashboard?id=dc-project)
|
||||||
|
|
||||||
[](https://github.com/flecomte/dc-project/actions/workflows/tests.yml)
|
[](https://github.com/flecomte/dc-project/actions/workflows/tests.yml)
|
||||||
|
|||||||
147
build.gradle.kts
147
build.gradle.kts
@@ -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.0"
|
val ktorVersion = "1.5.4"
|
||||||
val kotlinVersion = "1.4.30"
|
val kotlinVersion = "1.5.31"
|
||||||
val coroutinesVersion = "1.4.3"
|
val coroutinesVersion = "1.5.2"
|
||||||
val logbackVersion = "1.2.3"
|
val logbackVersion = "1.2.3"
|
||||||
val koinVersion = "2.0.1"
|
val koinVersion = "3.1.5"
|
||||||
val jacksonVersion = "2.12.1"
|
val jacksonVersion = "2.13.1"
|
||||||
|
|
||||||
group = "com.github.flecomte"
|
group = "com.github.flecomte"
|
||||||
version = versioning.info.run {
|
version = versioning.info.run {
|
||||||
@@ -28,20 +28,24 @@ version = versioning.info.run {
|
|||||||
plugins {
|
plugins {
|
||||||
jacoco
|
jacoco
|
||||||
application
|
application
|
||||||
maven
|
`maven-publish`
|
||||||
|
|
||||||
id("maven-publish")
|
kotlin("jvm") version "1.5.31"
|
||||||
kotlin("jvm") version "1.4.30"
|
kotlin("plugin.serialization") version "1.5.31"
|
||||||
kotlin("plugin.serialization") version "1.4.30"
|
|
||||||
|
|
||||||
id("com.github.johnrengelman.shadow") version "5.2.0"
|
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
|
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
|
||||||
id("org.owasp.dependencycheck") version "6.1.1"
|
id("org.owasp.dependencycheck") version "6.1.5"
|
||||||
id("org.sonarqube") version "3.1.1"
|
id("org.sonarqube") version "3.3"
|
||||||
id("net.nemerosa.versioning") version "2.14.0"
|
id("net.nemerosa.versioning") version "2.15.1"
|
||||||
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
|
id("io.gitlab.arturbosch.detekt") version "1.19.0"
|
||||||
id("com.avast.gradle.docker-compose") version "0.14.0"
|
id("com.avast.gradle.docker-compose") version "0.15.1"
|
||||||
id("com.github.kt3k.coveralls") version "2.8.4"
|
id("com.github.kt3k.coveralls") version "2.12.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyLocking {
|
||||||
|
lockAllConfigurations()
|
||||||
|
// lockMode.set(LockMode.STRICT)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -56,7 +60,7 @@ buildscript {
|
|||||||
maven { url = uri("https://jitpack.io") }
|
maven { url = uri("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.typesafe:config:1.4.1")
|
classpath("com.typesafe:config:1.4.2")
|
||||||
classpath("com.github.flecomte:postgres-json:2.1.2")
|
classpath("com.github.flecomte:postgres-json:2.1.2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +98,7 @@ val migration by tasks.registering {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val migrationTest by tasks.registering {
|
val migrationTest by tasks.registering {
|
||||||
group = "verification"
|
group = "tests"
|
||||||
dependsOn(tasks.named("testComposeUp"))
|
dependsOn(tasks.named("testComposeUp"))
|
||||||
finalizedBy(tasks.named("testComposeDown"))
|
finalizedBy(tasks.named("testComposeDown"))
|
||||||
doLast {
|
doLast {
|
||||||
@@ -118,11 +122,9 @@ val migrationTest by tasks.registering {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val testSql by tasks.registering {
|
val testSql by tasks.registering {
|
||||||
group = "verification"
|
group = "tests"
|
||||||
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()
|
||||||
@@ -167,6 +169,7 @@ tasks.withType<Jar> {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
isZip64 = true
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
@@ -180,11 +183,10 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +200,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,13 +207,6 @@ 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"
|
||||||
@@ -228,14 +222,12 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +258,7 @@ publishing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jacoco {
|
jacoco {
|
||||||
toolVersion = "0.8.6"
|
toolVersion = "0.8.7"
|
||||||
applyTo(tasks.run.get())
|
applyTo(tasks.run.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,6 +312,75 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -327,7 +388,6 @@ 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") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +405,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("org.koin:koin-ktor:$koinVersion")
|
implementation("io.insert-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")
|
||||||
@@ -359,17 +419,18 @@ 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("org.koin:koin-test:$koinVersion")
|
testImplementation("io.insert-koin:koin-test:$koinVersion")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||||
testImplementation("io.mockk:mockk:1.10.6")
|
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
|
testImplementation("org.amshove.kluent:kluent:1.68")
|
||||||
testImplementation("org.amshove.kluent:kluent:1.61")
|
testImplementation("io.mockk:mockk:1.12.2")
|
||||||
testImplementation("io.mockk:mockk-agent-api:1.10.6")
|
testImplementation("io.mockk:mockk-agent-api:1.12.2")
|
||||||
testImplementation("io.mockk:mockk-agent-jvm:1.10.6")
|
testImplementation("io.mockk:mockk-agent-jvm:1.12.2")
|
||||||
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")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: '3.8'
|
version: '3.3'
|
||||||
services:
|
services:
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
container_name: ${APP_NAME}_rabbitmq_test
|
container_name: ${APP_NAME}_rabbitmq_test
|
||||||
|
|||||||
275
gradle.lockfile
Normal file
275
gradle.lockfile
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# 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
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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-6.8-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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
|
||||||
@@ -25,8 +26,10 @@ 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.NotificationConsumer
|
import fr.dcproject.component.notification.email.NotificationEmailConsumer
|
||||||
|
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
|
||||||
@@ -37,7 +40,6 @@ 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
|
||||||
@@ -57,7 +59,6 @@ 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
|
||||||
@@ -72,7 +73,6 @@ 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,11 +117,14 @@ fun Application.module(env: Env = PROD) {
|
|||||||
masking = false
|
masking = false
|
||||||
}
|
}
|
||||||
|
|
||||||
get<NotificationConsumer>().run {
|
get<NotificationEmailConsumer>().run {
|
||||||
start()
|
start()
|
||||||
environment.monitor.subscribe(ApplicationStopped) {
|
onApplicationStopped { close() }
|
||||||
close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get<NotificationPushConsumer>().run {
|
||||||
|
start()
|
||||||
|
onApplicationStopped { close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
install(Authentication, jwtInstallation(get(), get()))
|
install(Authentication, jwtInstallation(get(), get()))
|
||||||
@@ -154,6 +157,7 @@ fun Application.module(env: Env = PROD) {
|
|||||||
installCommentRoutes()
|
installCommentRoutes()
|
||||||
installFollowArticleRoutes()
|
installFollowArticleRoutes()
|
||||||
installFollowConstitutionRoutes()
|
installFollowConstitutionRoutes()
|
||||||
|
installFollowCitizenRoutes()
|
||||||
installWorkgroupRoutes()
|
installWorkgroupRoutes()
|
||||||
installOpinionRoutes()
|
installOpinionRoutes()
|
||||||
installVoteRoutes()
|
installVoteRoutes()
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
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.util.KtorExperimentalAPI
|
import io.ktor.http.HttpStatusCode
|
||||||
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 ->
|
||||||
|
|||||||
@@ -10,21 +10,20 @@ 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.NotificationConsumer
|
import fr.dcproject.component.notification.NotificationPublisherAsync
|
||||||
import fr.dcproject.component.notification.NotificationEmailSender
|
import fr.dcproject.component.notification.email.NotificationEmailConsumer
|
||||||
import fr.dcproject.component.notification.NotificationsPush
|
import fr.dcproject.component.notification.email.NotificationEmailSender
|
||||||
import fr.dcproject.component.notification.Publisher
|
import fr.dcproject.component.notification.push.NotificationPushConsumer
|
||||||
|
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 {
|
||||||
@@ -65,11 +64,15 @@ val KoinModule = module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
single { NotificationsPush.Builder(get()) }
|
single { NotificationPushListener.Builder(get()) }
|
||||||
|
|
||||||
single {
|
single {
|
||||||
val config: Configuration = get()
|
val config: Configuration = get()
|
||||||
NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
|
NotificationEmailConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
|
NotificationPushConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RabbitMQ
|
// RabbitMQ
|
||||||
@@ -114,7 +117,7 @@ val KoinModule = module {
|
|||||||
|
|
||||||
single {
|
single {
|
||||||
val config: Configuration = get()
|
val config: Configuration = get()
|
||||||
Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
|
NotificationPublisherAsync(factory = get(), exchangeName = config.exchangeNotificationName)
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ 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
|
||||||
@@ -13,18 +14,10 @@ import java.util.concurrent.CompletionException
|
|||||||
|
|
||||||
class HttpError(
|
class HttpError(
|
||||||
statusCode: HttpStatusCode,
|
statusCode: HttpStatusCode,
|
||||||
val cause: Throwable? = null,
|
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 = {
|
||||||
@@ -79,4 +72,15 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ 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()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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
|
||||||
@@ -34,7 +35,8 @@ 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 {
|
||||||
@@ -44,6 +46,7 @@ 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ interface PaginatedRequestI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open class PaginatedRequest(
|
open class PaginatedRequest(
|
||||||
page: Int = 1,
|
override val page: Int = 1,
|
||||||
limit: Int = 50
|
override val 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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)
|
||||||
@@ -22,8 +21,6 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/main/kotlin/fr/dcproject/common/utils/Ktor.kt
Normal file
10
src/main/kotlin/fr/dcproject/common/utils/Ktor.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/kotlin/fr/dcproject/common/utils/RabbitConsume.kt
Normal file
30
src/main/kotlin/fr/dcproject/common/utils/RabbitConsume.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
41
src/main/kotlin/fr/dcproject/common/utils/retry.kt
Normal file
41
src/main/kotlin/fr/dcproject/common/utils/retry.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
6
src/main/kotlin/fr/dcproject/common/validation/Email.kt
Normal file
6
src/main/kotlin/fr/dcproject/common/validation/Email.kt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package fr.dcproject.common.validation
|
||||||
|
|
||||||
|
import io.konform.validation.ValidationBuilder
|
||||||
|
import io.konform.validation.jsonschema.pattern
|
||||||
|
|
||||||
|
fun ValidationBuilder<String>.email() = pattern(""".+@.+\..+""")
|
||||||
22
src/main/kotlin/fr/dcproject/common/validation/Password.kt
Normal file
22
src/main/kotlin/fr/dcproject/common/validation/Password.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
15
src/main/kotlin/fr/dcproject/common/validation/Url.kt
Normal file
15
src/main/kotlin/fr/dcproject/common/validation/Url.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main/kotlin/fr/dcproject/common/validation/Uuid.kt
Normal file
14
src/main/kotlin/fr/dcproject/common/validation/Uuid.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
.findVersionsByVersionId(1, 1, subject.versionId)
|
.findSiblingVersions(1, 1, subject)
|
||||||
.result
|
.result
|
||||||
.firstOrNull()?.createdBy?.id
|
.firstOrNull()?.createdBy?.id
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ data class ArticleForView(
|
|||||||
val lastVersion: Boolean = false
|
val lastVersion: Boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, VersionableId, TargetI, CreatedBy<C> {
|
sealed interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, VersionableId, TargetI, CreatedBy<C> {
|
||||||
val anonymous: Boolean
|
val anonymous: Boolean
|
||||||
val content: String
|
val content: String
|
||||||
val description: String
|
val description: String
|
||||||
@@ -56,13 +56,13 @@ interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, Versi
|
|||||||
val workgroup: WorkgroupRef?
|
val workgroup: WorkgroupRef?
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArticleForUpdate(
|
data class ArticleForUpdate(
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val anonymous: Boolean = true,
|
override val anonymous: Boolean = true,
|
||||||
override val content: String,
|
override val content: String,
|
||||||
override val description: String,
|
override val description: String,
|
||||||
tags: List<String> = emptyList(),
|
val tags: Set<String> = emptySet(),
|
||||||
override val draft: Boolean = false,
|
override val draft: Boolean = false,
|
||||||
override val createdBy: CitizenRef,
|
override val createdBy: CitizenRef,
|
||||||
override val workgroup: WorkgroupRef? = null,
|
override val workgroup: WorkgroupRef? = null,
|
||||||
@@ -71,12 +71,10 @@ class ArticleForUpdate(
|
|||||||
) : ArticleRef(id),
|
) : ArticleRef(id),
|
||||||
ArticleForUpdateI<CitizenRef>,
|
ArticleForUpdateI<CitizenRef>,
|
||||||
ArticleAuthI<CitizenRef>,
|
ArticleAuthI<CitizenRef>,
|
||||||
VersionableId {
|
VersionableId
|
||||||
val tags: List<String> = tags.distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArticleForListing(
|
data class ArticleForListing(
|
||||||
id: UUID? = null,
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val createdBy: CitizenCreator,
|
override val createdBy: CitizenCreator,
|
||||||
override val workgroup: WorkgroupCart? = null,
|
override val workgroup: WorkgroupCart? = null,
|
||||||
@@ -87,9 +85,10 @@ 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> {
|
sealed interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {
|
||||||
val workgroup: WorkgroupCartI?
|
val workgroup: WorkgroupCartI?
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +96,13 @@ open class ArticleRef(
|
|||||||
id: UUID? = null
|
id: UUID? = null
|
||||||
) : ArticleI, TargetRef(id)
|
) : ArticleI, TargetRef(id)
|
||||||
|
|
||||||
interface ArticleI : EntityI, TargetI
|
sealed interface ArticleI : EntityI, TargetI
|
||||||
|
|
||||||
interface ArticleWithTitleI : ArticleI {
|
sealed interface ArticleWithTitleI : ArticleI {
|
||||||
val title: String
|
val title: String
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArticleAuthI<U : CitizenI> :
|
sealed interface ArticleAuthI<U : CitizenI> :
|
||||||
ArticleI,
|
ArticleI,
|
||||||
CreatedBy<U>,
|
CreatedBy<U>,
|
||||||
DeletedAt {
|
DeletedAt {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -19,10 +20,10 @@ class ArticleRepository(override var requester: Requester) : RepositoryI {
|
|||||||
.select(page, limit, "id" to id)
|
.select(page, limit, "id" to id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForListing> {
|
fun <A> findSiblingVersions(page: Int = 1, limit: Int = 50, article: A): Paginated<ArticleForListing> where A : VersionableId, A : ArticleI {
|
||||||
return requester
|
return requester
|
||||||
.getFunction("find_articles_versions_by_version_id")
|
.getFunction("find_articles_versions_by_version_id")
|
||||||
.select(page, limit, "version_id" to versionId)
|
.select(page, limit, "version_id" to article.versionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun find(
|
fun find(
|
||||||
|
|||||||
@@ -1,44 +1,71 @@
|
|||||||
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(
|
||||||
article: UUID,
|
val article: String,
|
||||||
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) {
|
||||||
val page: Int = if (page < 1) 1 else page
|
fun validate() = Validation<ArticleVersionsRequest> {
|
||||||
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
|
ArticleVersionsRequest::page {
|
||||||
val article = ArticleRef(article)
|
minimum(1)
|
||||||
|
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.id)
|
findVersionsById(request.page, request.limit, request.article.toUUID())
|
||||||
|
|
||||||
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.canView(result, citizenOrNull).assert() }
|
||||||
.run {
|
.run {
|
||||||
call.respond(
|
call.respond(
|
||||||
toOutput { a: ArticleForListing ->
|
toOutput { a: ArticleForListing ->
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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
|
||||||
@@ -10,6 +12,10 @@ 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
|
||||||
@@ -28,7 +34,31 @@ 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(
|
||||||
@@ -43,14 +73,17 @@ 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.canView(result, citizenOrNull).assert() }
|
||||||
.let {
|
.let {
|
||||||
call.respond(
|
call.respond(
|
||||||
it.toOutput {
|
it.toOutput {
|
||||||
object {
|
object {
|
||||||
val id = it.id
|
val id = it.id
|
||||||
val title = it.title
|
val title = it.title
|
||||||
|
val createdAt = it.createdAt
|
||||||
val createdBy: Any = it.createdBy.toOutput()
|
val createdBy: Any = it.createdBy.toOutput()
|
||||||
val workgroup = it.workgroup?.let {
|
val workgroup = it.workgroup?.let {
|
||||||
object {
|
object {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ object GetOneArticle {
|
|||||||
fun Route.getOneArticle(viewRepository: ArticleViewRepository<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
|
fun Route.getOneArticle(viewRepository: ArticleViewRepository<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
|
||||||
get<ArticleRequest> {
|
get<ArticleRequest> {
|
||||||
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
|
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
|
||||||
ac.assert { canView(article, citizenOrNull) }
|
ac.canView(article, citizenOrNull).assert()
|
||||||
|
|
||||||
call.respond(
|
call.respond(
|
||||||
article.let { a ->
|
article.let { a ->
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -9,9 +10,14 @@ 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.ArticleUpdateNotification
|
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage
|
||||||
import fr.dcproject.component.notification.Publisher
|
import fr.dcproject.component.notification.NotificationPublisherAsync
|
||||||
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
|
||||||
@@ -31,15 +37,35 @@ object UpsertArticle {
|
|||||||
val anonymous: Boolean = true,
|
val anonymous: Boolean = true,
|
||||||
val content: String,
|
val content: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val tags: List<String> = emptyList(),
|
val tags: Set<String> = emptySet(),
|
||||||
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, publisher: Publisher, ac: ArticleAccessControl) {
|
fun Route.upsertArticle(repo: ArticleRepository, notificationPublisher: NotificationPublisherAsync, 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,
|
||||||
@@ -57,7 +83,7 @@ object UpsertArticle {
|
|||||||
post<UpsertArticleRequest> {
|
post<UpsertArticleRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val article = call.convertRequestToEntity()
|
val article = call.convertRequestToEntity()
|
||||||
ac.assert { canUpsert(article, citizenOrNull) }
|
ac.canUpsert(article, citizenOrNull).assert()
|
||||||
repo.upsert(article)?.let { a ->
|
repo.upsert(article)?.let { a ->
|
||||||
call.respond(
|
call.respond(
|
||||||
object {
|
object {
|
||||||
@@ -66,7 +92,7 @@ object UpsertArticle {
|
|||||||
val versionNumber = a.versionNumber
|
val versionNumber = a.versionNumber
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
publisher.publish(ArticleUpdateNotification(a))
|
notificationPublisher.publishAsync(ArticleUpdateNotificationMessage(a))
|
||||||
} ?: error("Article not updated")
|
} ?: error("Article not updated")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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().koin.get<CitizenRepository>().findByUser(user)
|
GlobalContext.get().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().koin.get<CitizenRepository>().findByUser(it)
|
GlobalContext.get().get<CitizenRepository>().findByUser(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ApplicationCall.isAuth: Boolean
|
val ApplicationCall.isAuth: Boolean
|
||||||
|
|||||||
@@ -9,35 +9,45 @@ import io.ktor.auth.Principal
|
|||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class UserForCreate(
|
data class UserForCreate(
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
username: String,
|
override val username: String,
|
||||||
override val password: String,
|
override val password: String,
|
||||||
blockedAt: DateTime? = null,
|
override val blockedAt: DateTime? = null,
|
||||||
roles: List<Roles> = emptyList()
|
override val roles: Set<Roles> = emptySet()
|
||||||
) : User(id, username, blockedAt, roles),
|
) : UserForViewI,
|
||||||
UserWithPasswordI
|
UserWithPasswordI,
|
||||||
|
CreatedAt by CreatedAt.Imp(),
|
||||||
|
UpdatedAt by UpdatedAt.Imp()
|
||||||
|
|
||||||
open class User(
|
data class User(
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override var username: String,
|
override val username: String,
|
||||||
var blockedAt: DateTime? = null,
|
override val blockedAt: DateTime? = null,
|
||||||
var roles: List<Roles> = emptyList()
|
override val roles: Set<Roles> = emptySet()
|
||||||
) : UserRef(id),
|
) : UserRef(id),
|
||||||
|
UserForViewI,
|
||||||
UserWithUsername,
|
UserWithUsername,
|
||||||
CreatedAt by CreatedAt.Imp(),
|
CreatedAt by CreatedAt.Imp(),
|
||||||
UpdatedAt by UpdatedAt.Imp()
|
UpdatedAt by UpdatedAt.Imp()
|
||||||
|
|
||||||
|
sealed interface UserForViewI :
|
||||||
|
UserI,
|
||||||
|
UserWithUsername,
|
||||||
|
UserForAuthI,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt
|
||||||
|
|
||||||
class UserCreator(
|
class UserCreator(
|
||||||
id: UUID = UUID.randomUUID(),
|
id: UUID = UUID.randomUUID(),
|
||||||
override val username: String,
|
override val username: String,
|
||||||
) : UserRef(id), UserWithUsername
|
) : UserRef(id), UserWithUsername
|
||||||
|
|
||||||
interface UserWithUsername : UserI {
|
sealed interface UserWithUsername : UserI {
|
||||||
val username: String
|
val username: String
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserWithPasswordI : UserI {
|
sealed interface UserWithPasswordI : UserI {
|
||||||
val password: String
|
val password: String
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +61,11 @@ open class UserRef(
|
|||||||
id: UUID = UUID.randomUUID()
|
id: UUID = UUID.randomUUID()
|
||||||
) : UserI, Entity(id)
|
) : UserI, Entity(id)
|
||||||
|
|
||||||
interface UserI : EntityI, Principal {
|
sealed interface UserI : EntityI, Principal {
|
||||||
enum class Roles { ROLE_USER, ROLE_ADMIN }
|
enum class Roles { ROLE_USER, ROLE_ADMIN }
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserForAuthI : UserI {
|
sealed interface UserForAuthI : UserI {
|
||||||
var roles: List<Roles>
|
val roles: Set<Roles>
|
||||||
var blockedAt: DateTime?
|
val blockedAt: DateTime?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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().koin.get<JwtConfig>().run {
|
fun UserI.makeToken(): String = GlobalContext.get().get<JwtConfig>().run {
|
||||||
JWT.create()
|
JWT.create()
|
||||||
.withSubject("Authentication")
|
.withSubject("Authentication")
|
||||||
.withIssuer(issuer)
|
.withIssuer(issuer)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
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
|
||||||
@@ -9,6 +12,9 @@ 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
|
||||||
@@ -43,6 +49,35 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +91,16 @@ object Register {
|
|||||||
user = UserForCreate(
|
user = UserForCreate(
|
||||||
username = user.username,
|
username = user.username,
|
||||||
password = user.password,
|
password = user.password,
|
||||||
roles = listOf(UserI.Roles.ROLE_USER)
|
roles = setOf(UserI.Roles.ROLE_USER)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
post<RegisterRequest> {
|
post<RegisterRequest> {
|
||||||
try {
|
try {
|
||||||
val citizen = call.receiveOrBadRequest<Input>().toCitizen()
|
val citizen = call.receiveOrBadRequest<Input>()
|
||||||
|
.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(
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ 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
|
||||||
@@ -16,19 +17,19 @@ import fr.postgresjson.entity.Serializable
|
|||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class CitizenForCreate(
|
data class CitizenForCreate(
|
||||||
val name: Name,
|
val name: Name,
|
||||||
val email: String,
|
val email: String,
|
||||||
val birthday: DateTime,
|
val birthday: DateTime,
|
||||||
val voteAnonymous: Boolean = true,
|
val voteAnonymous: Boolean = true,
|
||||||
val followAnonymous: Boolean = true,
|
val followAnonymous: Boolean = true,
|
||||||
override val user: UserForCreate,
|
override val user: UserForCreate,
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : CitizenI,
|
) : CitizenI,
|
||||||
CitizenRefWithUser(id, user),
|
CitizenWithUserI by CitizenRefWithUser(id, user),
|
||||||
CreatedAt by CreatedAt.Imp()
|
CreatedAt by CreatedAt.Imp()
|
||||||
|
|
||||||
class Citizen(
|
data class Citizen(
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val name: Name,
|
override val name: Name,
|
||||||
override val email: String,
|
override val email: String,
|
||||||
@@ -36,7 +37,7 @@ class Citizen(
|
|||||||
override val voteAnonymous: Boolean = true,
|
override val voteAnonymous: Boolean = true,
|
||||||
override val followAnonymous: Boolean = true,
|
override val followAnonymous: Boolean = true,
|
||||||
override val user: User,
|
override val user: User,
|
||||||
deletedAt: DateTime? = null
|
override val deletedAt: DateTime? = null
|
||||||
) : CitizenWithEmail,
|
) : CitizenWithEmail,
|
||||||
CitizenCreatorI,
|
CitizenCreatorI,
|
||||||
CitizenWithUserI,
|
CitizenWithUserI,
|
||||||
@@ -61,10 +62,11 @@ data class CitizenCreator(
|
|||||||
override val user: UserCreator,
|
override val user: UserCreator,
|
||||||
override val deletedAt: DateTime? = null
|
override val deletedAt: DateTime? = null
|
||||||
) : CitizenCreatorI,
|
) : CitizenCreatorI,
|
||||||
CitizenRefWithUser(id, user),
|
CitizenI,
|
||||||
|
CitizenWithUserI by CitizenRefWithUser(id, user),
|
||||||
DeletedAt by DeletedAt.Imp(deletedAt)
|
DeletedAt by DeletedAt.Imp(deletedAt)
|
||||||
|
|
||||||
interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, DeletedAt {
|
sealed interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, DeletedAt {
|
||||||
override val id: UUID
|
override val id: UUID
|
||||||
override val name: Name
|
override val name: Name
|
||||||
override val email: String
|
override val email: String
|
||||||
@@ -74,8 +76,8 @@ interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, De
|
|||||||
override val deletedAt: DateTime?
|
override val deletedAt: DateTime?
|
||||||
}
|
}
|
||||||
|
|
||||||
class CitizenCart(
|
data class CitizenCart(
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val name: Name,
|
override val name: Name,
|
||||||
override val user: UserRef,
|
override val user: UserRef,
|
||||||
override val deletedAt: DateTime? = null,
|
override val deletedAt: DateTime? = null,
|
||||||
@@ -83,22 +85,22 @@ class CitizenCart(
|
|||||||
CitizenCartI,
|
CitizenCartI,
|
||||||
DeletedAt by DeletedAt.Imp(deletedAt)
|
DeletedAt by DeletedAt.Imp(deletedAt)
|
||||||
|
|
||||||
interface CitizenCartI : CitizenI, CitizenWithUserI {
|
sealed interface CitizenCartI : CitizenI, CitizenWithUserI {
|
||||||
val name: Name
|
val name: Name
|
||||||
}
|
}
|
||||||
|
|
||||||
open class CitizenRefWithUser(
|
data class CitizenRefWithUser(
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val user: UserRef
|
override val user: UserI
|
||||||
) : CitizenWithUserI,
|
) : CitizenWithUserI,
|
||||||
CitizenRef(id)
|
CitizenRef(id)
|
||||||
|
|
||||||
open class CitizenRef(
|
open class CitizenRef(
|
||||||
id: UUID = UUID.randomUUID()
|
id: UUID = UUID.randomUUID()
|
||||||
) : Entity(id),
|
) : TargetRef(id),
|
||||||
CitizenI
|
CitizenI
|
||||||
|
|
||||||
interface CitizenI : EntityI {
|
sealed interface CitizenI : EntityI, TargetI {
|
||||||
data class Name(
|
data class Name(
|
||||||
override val firstName: String,
|
override val firstName: String,
|
||||||
override val lastName: String,
|
override val lastName: String,
|
||||||
@@ -113,10 +115,10 @@ interface CitizenI : EntityI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CitizenWithUserI : CitizenI {
|
sealed interface CitizenWithUserI : CitizenI {
|
||||||
val user: UserI
|
val user: UserI
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CitizenWithEmail : CitizenI {
|
sealed interface CitizenWithEmail : CitizenI {
|
||||||
val email: String
|
val email: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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
|
||||||
@@ -9,6 +11,7 @@ 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
|
||||||
@@ -25,14 +28,21 @@ object ChangeMyPassword {
|
|||||||
@Location("/citizens/{citizen}/password/change")
|
@Location("/citizens/{citizen}/password/change")
|
||||||
class ChangePasswordCitizenRequest(citizen: UUID) {
|
class ChangePasswordCitizenRequest(citizen: UUID) {
|
||||||
val citizen = CitizenRef(citizen)
|
val citizen = CitizenRef(citizen)
|
||||||
data class Input(val oldPassword: String, val newPassword: String)
|
data class Input(val oldPassword: String, val newPassword: String) {
|
||||||
|
fun validate() = Validation<Input> {
|
||||||
|
Input::newPassword {
|
||||||
|
passwordScore(15)
|
||||||
|
}
|
||||||
|
}.validate(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) {
|
fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) {
|
||||||
put<ChangePasswordCitizenRequest> {
|
put<ChangePasswordCitizenRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
ac.assert { canChangePassword(it.citizen, citizenOrNull) }
|
|
||||||
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
|
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
|
||||||
|
.apply { validate().badRequestIfNotValid() }
|
||||||
|
ac.canChangePassword(it.citizen, citizenOrNull).assert()
|
||||||
userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password")
|
userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password")
|
||||||
userRepository.changePassword(
|
userRepository.changePassword(
|
||||||
UserWithPassword(
|
UserWithPassword(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -10,6 +11,10 @@ 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
|
||||||
@@ -27,13 +32,30 @@ 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.canView(citizens.result, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
citizens.toOutput { c: CitizenCreator ->
|
citizens.toOutput { c: CitizenCreator ->
|
||||||
object {
|
object {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ object GetCurrentCitizen {
|
|||||||
if (currentUser === null) {
|
if (currentUser === null) {
|
||||||
call.respond(HttpStatusCode.Unauthorized)
|
call.respond(HttpStatusCode.Unauthorized)
|
||||||
} else {
|
} else {
|
||||||
ac.assert { canView(currentUser, citizenOrNull) }
|
ac.canView(currentUser, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
object {
|
object {
|
||||||
val id: UUID = citizen.id
|
val id: UUID = citizen.id
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ object GetOneCitizen {
|
|||||||
get<CitizenRequest> {
|
get<CitizenRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val citizen = citizenRepository.findById(it.citizen.id) ?: throw NotFoundException("Citizen not found ${it.citizen.id}")
|
val citizen = citizenRepository.findById(it.citizen.id) ?: throw NotFoundException("Citizen not found ${it.citizen.id}")
|
||||||
ac.assert { canView(citizen, citizenOrNull) }
|
ac.canView(citizen, citizenOrNull).assert()
|
||||||
|
|
||||||
call.respond(
|
call.respond(
|
||||||
object {
|
object {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
|
|||||||
target: EntityI,
|
target: EntityI,
|
||||||
page: Int,
|
page: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
sort: Sort
|
sort: String
|
||||||
): Paginated<CommentForView<ArticleForView, CitizenCreatorI>> {
|
): Paginated<CommentForView<ArticleForView, CitizenCreatorI>> {
|
||||||
return requester
|
return requester
|
||||||
.getFunction("find_comments_by_target")
|
.getFunction("find_comments_by_target")
|
||||||
@@ -49,18 +49,7 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
|
|||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
"target_id" to target.id,
|
"target_id" to target.id,
|
||||||
"sort" to sort.sql
|
"sort" to sort
|
||||||
) 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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package fr.dcproject.component.comment.article.routes
|
package fr.dcproject.component.comment.article.routes
|
||||||
|
|
||||||
import fr.dcproject.common.response.toOutput
|
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
|
||||||
@@ -12,6 +12,9 @@ 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
|
||||||
@@ -26,20 +29,29 @@ 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>().run {
|
call.receiveOrBadRequest<Input>()
|
||||||
|
.apply { validate().badRequestIfNotValid() }
|
||||||
|
.run {
|
||||||
CommentForUpdate(
|
CommentForUpdate(
|
||||||
target = it.article,
|
target = it.article,
|
||||||
createdBy = citizen,
|
createdBy = citizen,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}.let { comment ->
|
}.let { comment ->
|
||||||
ac.assert { canCreate(comment, citizenOrNull) }
|
ac.canCreate(comment, citizenOrNull).assert()
|
||||||
repo.comment(comment)
|
repo.comment(comment)
|
||||||
|
|
||||||
call.respond(
|
call.respond(
|
||||||
|
|||||||
@@ -1,5 +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.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
|
||||||
@@ -9,6 +10,10 @@ 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
|
||||||
@@ -26,17 +31,34 @@ object GetArticleComments {
|
|||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
limit: Int = 50,
|
limit: Int = 50,
|
||||||
val search: String? = null,
|
val search: String? = null,
|
||||||
sort: String = CommentArticleRepository.Sort.CREATED_AT.sql
|
val sort: String = "createdAt"
|
||||||
) : 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.canView(comments.result, citizenOrNull).assert()
|
||||||
}
|
}
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ object GetCitizenArticleComments {
|
|||||||
get<CitizenCommentArticleRequest> {
|
get<CitizenCommentArticleRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
repo.findByCitizen(it.citizen).let { comments ->
|
repo.findByCitizen(it.citizen).let { comments ->
|
||||||
ac.assert { canView(comments.result, citizenOrNull) }
|
ac.canView(comments.result, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
comments.toOutput { comment ->
|
comments.toOutput { comment ->
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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
|
||||||
@@ -41,7 +40,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
|
|||||||
target: EntityI,
|
target: EntityI,
|
||||||
page: Int,
|
page: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
sort: CommentArticleRepository.Sort
|
sort: String
|
||||||
): Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> {
|
): Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> {
|
||||||
return requester.run {
|
return requester.run {
|
||||||
getFunction("find_comments_by_target")
|
getFunction("find_comments_by_target")
|
||||||
@@ -49,7 +48,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
|
|||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
"target_id" to target.id,
|
"target_id" to target.id,
|
||||||
"sort" to sort.sql
|
"sort" to sort
|
||||||
)
|
)
|
||||||
as Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>>
|
as Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -12,6 +13,9 @@ 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
|
||||||
@@ -26,20 +30,30 @@ object CreateConstitutionComment {
|
|||||||
@Location("/constitutions/{constitution}/comments")
|
@Location("/constitutions/{constitution}/comments")
|
||||||
class CreateConstitutionCommentRequest(constitution: UUID) {
|
class CreateConstitutionCommentRequest(constitution: UUID) {
|
||||||
val constitution = ConstitutionRef(constitution)
|
val constitution = ConstitutionRef(constitution)
|
||||||
class Input(val content: String)
|
class Input(val content: String) {
|
||||||
|
fun validate() = Validation<Input> {
|
||||||
|
Input::content {
|
||||||
|
minLength(20)
|
||||||
|
maxLength(6000)
|
||||||
|
}
|
||||||
|
}.validate(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
|
fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
|
||||||
post<CreateConstitutionCommentRequest> {
|
post<CreateConstitutionCommentRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
call.receiveOrBadRequest<Input>().run {
|
|
||||||
|
call.receiveOrBadRequest<Input>()
|
||||||
|
.apply { validate().badRequestIfNotValid() }
|
||||||
|
.run {
|
||||||
CommentForUpdate(
|
CommentForUpdate(
|
||||||
target = it.constitution,
|
target = it.constitution,
|
||||||
createdBy = citizen,
|
createdBy = citizen,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}.let { comment ->
|
}.let { comment ->
|
||||||
ac.assert { canCreate(comment, citizenOrNull) }
|
ac.canCreate(comment, citizenOrNull).assert()
|
||||||
repo.comment(comment)
|
repo.comment(comment)
|
||||||
|
|
||||||
call.respond(
|
call.respond(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ object GetCitizenCommentConstitution {
|
|||||||
get<GetCitizenCommentConstitutionRequest> {
|
get<GetCitizenCommentConstitutionRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val comments = repo.findByCitizen(it.citizen)
|
val comments = repo.findByCitizen(it.citizen)
|
||||||
ac.assert { canView(comments.result, citizenOrNull) }
|
ac.canView(comments.result, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
comments.toOutput { comment ->
|
comments.toOutput { comment ->
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -7,6 +8,12 @@ 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
|
||||||
@@ -19,14 +26,38 @@ import java.util.UUID
|
|||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
object GetConstitutionComment {
|
object GetConstitutionComment {
|
||||||
@Location("/constitutions/{constitution}/comments")
|
@Location("/constitutions/{constitution}/comments")
|
||||||
class GetConstitutionCommentRequest(constitution: UUID) {
|
class GetConstitutionCommentRequest(
|
||||||
|
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.canView(comments.result, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
comments.toOutput { comment ->
|
comments.toOutput { comment ->
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import fr.dcproject.component.vote.entity.VotableImp
|
|||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class CommentForView<T : TargetI, C : CitizenCreatorI>(
|
data class CommentForView<T : TargetI, C : CitizenCreatorI>(
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val createdBy: C,
|
override val createdBy: C,
|
||||||
override val target: T,
|
override val target: T,
|
||||||
override var content: String,
|
override var content: String,
|
||||||
@@ -30,7 +30,7 @@ class CommentForView<T : TargetI, C : CitizenCreatorI>(
|
|||||||
CommentWithTargetI<T>,
|
CommentWithTargetI<T>,
|
||||||
CreatedBy<C> by CreatedBy.Imp(createdBy),
|
CreatedBy<C> by CreatedBy.Imp(createdBy),
|
||||||
UpdatedAt by UpdatedAt.Imp(),
|
UpdatedAt by UpdatedAt.Imp(),
|
||||||
DeletedAt by DeletedAt.Imp(),
|
DeletedAt,
|
||||||
Votable by VotableImp(),
|
Votable by VotableImp(),
|
||||||
TargetI {
|
TargetI {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -50,9 +50,9 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
|
|||||||
override val createdBy: C,
|
override val createdBy: C,
|
||||||
override val target: T,
|
override val target: T,
|
||||||
open var content: String,
|
open var content: String,
|
||||||
override val parent: CommentParent<T>? = null,
|
override val parent: CommentParentI<T>? = null,
|
||||||
override val deletedAt: DateTime? = null
|
override val deletedAt: DateTime? = null
|
||||||
) : CommentParent<T>(id, deletedAt, target),
|
) : CommentParentI<T> by CommentParent(id, deletedAt, target),
|
||||||
CommentWithParentI<T>,
|
CommentWithParentI<T>,
|
||||||
ExtraI<T, C>,
|
ExtraI<T, C>,
|
||||||
CommentWithTargetI<T>,
|
CommentWithTargetI<T>,
|
||||||
@@ -62,31 +62,33 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
|
|||||||
TargetI {
|
TargetI {
|
||||||
constructor(
|
constructor(
|
||||||
createdBy: C,
|
createdBy: C,
|
||||||
parent: CommentParent<T>,
|
parent: CommentParentI<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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
open class CommentParent<T : TargetI>(
|
data class CommentParent<T : TargetI>(
|
||||||
override val id: UUID,
|
override val id: UUID,
|
||||||
override val deletedAt: DateTime?,
|
override val deletedAt: DateTime?,
|
||||||
override val target: T
|
override val target: T
|
||||||
) : CommentRef(id),
|
) : CommentRef(id),
|
||||||
CommentParentI<T>
|
CommentParentI<T>
|
||||||
|
|
||||||
interface CommentParentI<T : TargetI> : CommentI, DeletedAt, CommentWithTargetI<T>
|
sealed interface CommentParentI<T : TargetI> : CommentI, DeletedAt, CommentWithTargetI<T>
|
||||||
|
|
||||||
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, HasTarget<T>
|
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, HasTarget<T>
|
||||||
|
|
||||||
interface CommentWithParentI<T : TargetI> {
|
interface CommentWithParentI<T : TargetI> {
|
||||||
val parent: CommentParent<T>?
|
val parent: CommentParentI<T>?
|
||||||
}
|
}
|
||||||
|
|
||||||
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id)
|
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id)
|
||||||
|
|
||||||
interface CommentI : EntityI
|
sealed interface CommentI : EntityI
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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
|
||||||
@@ -22,7 +21,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: CommentForView<T, CitizenCreatorI>,
|
parent: CommentI,
|
||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
limit: Int = 50
|
limit: Int = 50
|
||||||
): Paginated<CommentForView<T, CitizenCreatorI>> {
|
): Paginated<CommentForView<T, CitizenCreatorI>> {
|
||||||
@@ -33,100 +32,81 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
|
|||||||
parentId: UUID,
|
parentId: UUID,
|
||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
limit: Int = 50
|
limit: Int = 50
|
||||||
): Paginated<CommentForView<T, CitizenCreatorI>> {
|
): Paginated<CommentForView<T, CitizenCreatorI>> = requester
|
||||||
return requester.run {
|
.getFunction("find_comments_by_parent")
|
||||||
getFunction("find_comments_by_parent")
|
|
||||||
.select<CommentForView<T, CitizenCreator>>(
|
.select<CommentForView<T, CitizenCreator>>(
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
"parent_id" to parentId
|
"parent_id" to parentId
|
||||||
)
|
)
|
||||||
as Paginated<CommentForView<T, CitizenCreatorI>>
|
as Paginated<CommentForView<T, CitizenCreatorI>>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun findByTarget(
|
open fun findByTarget(
|
||||||
target: EntityI,
|
target: EntityI,
|
||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
limit: Int = 50,
|
limit: Int = 50,
|
||||||
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
|
sort: String = "createdAt"
|
||||||
): Paginated<CommentForView<T, CitizenCreatorI>> {
|
): Paginated<CommentForView<T, CitizenCreatorI>> = findByTarget(target.id, page, limit, sort)
|
||||||
return findByTarget(target.id, page, limit, sort)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun findByTarget(
|
open fun findByTarget(
|
||||||
targetId: UUID,
|
targetId: UUID,
|
||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
limit: Int = 50,
|
limit: Int = 50,
|
||||||
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
|
sort: String = "createdAt"
|
||||||
): Paginated<CommentForView<T, CitizenCreatorI>> {
|
): Paginated<CommentForView<T, CitizenCreatorI>> = requester
|
||||||
return requester.run {
|
.getFunction("find_comments_by_target")
|
||||||
getFunction("find_comments_by_target")
|
|
||||||
.select<CommentForView<T, CitizenCreator>>(
|
.select<CommentForView<T, CitizenCreator>>(
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
"target_id" to targetId,
|
"target_id" to targetId,
|
||||||
"sort" to sort.sql
|
"sort" to sort
|
||||||
)
|
) as Paginated<CommentForView<T, CitizenCreatorI>>
|
||||||
as Paginated<CommentForView<T, CitizenCreatorI>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>) {
|
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>): CommentForView<TargetRef, CitizenCreator> = requester
|
||||||
requester
|
|
||||||
.getFunction("comment")
|
.getFunction("comment")
|
||||||
.sendQuery(
|
.selectOne(
|
||||||
"reference" to comment.target.reference,
|
"reference" to comment.target.reference,
|
||||||
"resource" to comment
|
"resource" to comment
|
||||||
)
|
)!!
|
||||||
}
|
|
||||||
|
|
||||||
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>) {
|
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>): CommentForView<TargetRef, CitizenCreator> {
|
||||||
requester
|
return requester
|
||||||
.getFunction("edit_comment")
|
.getFunction("edit_comment")
|
||||||
.sendQuery(
|
.selectOne(
|
||||||
"id" to comment.id,
|
"id" to comment.id,
|
||||||
"content" to comment.content
|
"content" to comment.content
|
||||||
)
|
)!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CommentRepository(requester: Requester) : CommentRepositoryAbs<TargetRef>(requester) {
|
class CommentRepository(requester: Requester) : CommentRepositoryAbs<TargetRef>(requester) {
|
||||||
override fun findById(id: UUID): CommentForView<TargetRef, CitizenCreatorI>? {
|
override fun findById(id: UUID): CommentForView<TargetRef, CitizenCreatorI>? = requester
|
||||||
return requester
|
|
||||||
.getFunction("find_comment_by_id")
|
.getFunction("find_comment_by_id")
|
||||||
.selectOne<CommentForView<TargetRef, CitizenCreator>>(mapOf("id" to id))
|
.selectOne<CommentForView<TargetRef, CitizenCreator>>(mapOf("id" to id))
|
||||||
as CommentForView<TargetRef, CitizenCreatorI>?
|
as CommentForView<TargetRef, CitizenCreatorI>?
|
||||||
}
|
|
||||||
|
|
||||||
override fun findByCitizen(
|
override fun findByCitizen(
|
||||||
citizen: CitizenI,
|
citizen: CitizenI,
|
||||||
page: Int,
|
page: Int,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> {
|
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> = requester
|
||||||
return requester.run {
|
.getFunction("find_comments_by_citizen")
|
||||||
getFunction("find_comments_by_citizen")
|
|
||||||
.select<CommentForView<TargetRef, CitizenCreator>>(
|
.select<CommentForView<TargetRef, CitizenCreator>>(
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
"created_by_id" to citizen.id
|
"created_by_id" to citizen.id
|
||||||
) as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
|
) as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findByParent(
|
override fun findByParent(
|
||||||
parentId: UUID,
|
parentId: UUID,
|
||||||
page: Int,
|
page: Int,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> {
|
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> = requester
|
||||||
return requester.run {
|
.getFunction("find_comments_by_parent")
|
||||||
getFunction("find_comments_by_parent")
|
|
||||||
.select<CommentForView<TargetRef, CitizenCreator>>(
|
.select<CommentForView<TargetRef, CitizenCreator>>(
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
"parent_id" to parentId
|
"parent_id" to parentId
|
||||||
)
|
)
|
||||||
as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
|
as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package fr.dcproject.component.comment.generic.routes
|
||||||
|
|
||||||
|
import fr.dcproject.application.http.badRequestIfNotValid
|
||||||
|
import fr.dcproject.common.security.assert
|
||||||
|
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||||
|
import fr.dcproject.component.auth.citizen
|
||||||
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
|
import fr.dcproject.component.auth.mustBeAuth
|
||||||
|
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||||
|
import fr.dcproject.component.comment.generic.database.CommentForUpdate
|
||||||
|
import fr.dcproject.component.comment.generic.database.CommentRef
|
||||||
|
import fr.dcproject.component.comment.generic.database.CommentRepository
|
||||||
|
import fr.dcproject.component.comment.toOutput
|
||||||
|
import io.konform.validation.Validation
|
||||||
|
import io.konform.validation.jsonschema.maxLength
|
||||||
|
import io.konform.validation.jsonschema.minLength
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.features.NotFoundException
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
|
import io.ktor.locations.Location
|
||||||
|
import io.ktor.locations.post
|
||||||
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@KtorExperimentalLocationsAPI
|
||||||
|
object CreateComment {
|
||||||
|
@Location("/comments/{comment}")
|
||||||
|
class CreateCommentRequest(comment: UUID) {
|
||||||
|
val comment = CommentRef(comment)
|
||||||
|
class Input(val content: String) {
|
||||||
|
fun validate() = Validation<Input> {
|
||||||
|
Input::content {
|
||||||
|
minLength(20)
|
||||||
|
maxLength(6000)
|
||||||
|
}
|
||||||
|
}.validate(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
|
||||||
|
post<CreateCommentRequest> {
|
||||||
|
mustBeAuth()
|
||||||
|
|
||||||
|
call.receiveOrBadRequest<CreateCommentRequest.Input>()
|
||||||
|
.apply { validate().badRequestIfNotValid() }
|
||||||
|
.run {
|
||||||
|
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
||||||
|
CommentForUpdate(
|
||||||
|
content = content,
|
||||||
|
createdBy = citizen,
|
||||||
|
target = parent.target,
|
||||||
|
parent = parent,
|
||||||
|
)
|
||||||
|
}.let { newComment ->
|
||||||
|
ac.canCreate(newComment, citizenOrNull).assert()
|
||||||
|
repo.comment(newComment)
|
||||||
|
call.respond(HttpStatusCode.Created, newComment.toOutput())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
package fr.dcproject.component.comment.generic.routes
|
package fr.dcproject.component.comment.generic.routes
|
||||||
|
|
||||||
import fr.dcproject.common.response.toOutput
|
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.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
|
||||||
@@ -24,22 +28,40 @@ 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 comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
||||||
ac.assert { canUpdate(comment, citizenOrNull) }
|
ac.canUpdate(commentOld, citizenOrNull).assert()
|
||||||
|
|
||||||
comment.content = call.receiveOrBadRequest<EditCommentRequest.Input>().content
|
|
||||||
repo.edit(comment)
|
|
||||||
|
|
||||||
|
call.receiveOrBadRequest<EditCommentRequest.Input>()
|
||||||
|
.apply { validate().badRequestIfNotValid() }
|
||||||
|
.run {
|
||||||
|
CommentForUpdate(
|
||||||
|
id = commentOld.id,
|
||||||
|
createdBy = commentOld.createdBy,
|
||||||
|
target = commentOld.target,
|
||||||
|
parent = commentOld.parent,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.let { repo.edit(it) }
|
||||||
|
.let {
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
comment.toOutput()
|
it.toOutput()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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
|
||||||
@@ -21,11 +22,13 @@ import java.util.UUID
|
|||||||
object GetCommentChildren {
|
object GetCommentChildren {
|
||||||
@Location("/comments/{comment}/children")
|
@Location("/comments/{comment}/children")
|
||||||
class CommentChildrenRequest(
|
class CommentChildrenRequest(
|
||||||
val comment: UUID,
|
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> {
|
||||||
@@ -36,7 +39,7 @@ object GetCommentChildren {
|
|||||||
it.limit
|
it.limit
|
||||||
)
|
)
|
||||||
|
|
||||||
ac.assert { canView(comments.result, citizenOrNull) }
|
ac.canView(comments.result, citizenOrNull).assert()
|
||||||
|
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ object GetOneComment {
|
|||||||
fun Route.getOneComment(repo: CommentRepository, ac: CommentAccessControl) {
|
fun Route.getOneComment(repo: CommentRepository, ac: CommentAccessControl) {
|
||||||
get<CommentRequest> {
|
get<CommentRequest> {
|
||||||
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
|
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
|
||||||
ac.assert { canView(comment, citizenOrNull) }
|
ac.canView(comment, citizenOrNull).assert()
|
||||||
|
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
|
|||||||
@@ -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.CreateCommentChildren.createCommentChildren
|
import fr.dcproject.component.comment.generic.routes.CreateComment.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
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -15,6 +16,9 @@ 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
|
||||||
@@ -36,7 +40,6 @@ 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,
|
||||||
@@ -44,10 +47,25 @@ 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,
|
||||||
@@ -71,7 +89,7 @@ object CreateConstitution {
|
|||||||
post<PostConstitutionRequest> {
|
post<PostConstitutionRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
getNewConstitution(call.receiveOrBadRequest(), citizen).let {
|
getNewConstitution(call.receiveOrBadRequest(), citizen).let {
|
||||||
ac.assert { canCreate(it, citizenOrNull) }
|
ac.canCreate(it, citizenOrNull).assert()
|
||||||
val c = repo.upsert(it) ?: error("Unable to create Constitution")
|
val c = repo.upsert(it) ?: error("Unable to create Constitution")
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.Created,
|
HttpStatusCode.Created,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -8,6 +9,10 @@ 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
|
||||||
@@ -27,12 +32,29 @@ 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.canView(constitutions.result, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
constitutions.toOutput { c ->
|
constitutions.toOutput { c ->
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ object GetConstitution {
|
|||||||
fun Route.getConstitution(ac: ConstitutionAccessControl, constitutionRepo: ConstitutionRepository) {
|
fun Route.getConstitution(ac: ConstitutionAccessControl, constitutionRepo: ConstitutionRepository) {
|
||||||
get<GetConstitutionRequest> {
|
get<GetConstitutionRequest> {
|
||||||
val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}")
|
val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}")
|
||||||
ac.assert { canView(constitution, citizenOrNull) }
|
ac.canView(constitution, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
constitution.let { c ->
|
constitution.let { c ->
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ 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"))
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ 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() {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
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() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,19 +8,18 @@ import fr.dcproject.common.entity.HasTarget
|
|||||||
import fr.dcproject.common.entity.TargetI
|
import fr.dcproject.common.entity.TargetI
|
||||||
import fr.dcproject.component.citizen.database.CitizenCreator
|
import fr.dcproject.component.citizen.database.CitizenCreator
|
||||||
import fr.dcproject.component.citizen.database.CitizenI
|
import fr.dcproject.component.citizen.database.CitizenI
|
||||||
import fr.dcproject.component.citizen.database.CitizenRef
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
open class FollowForView<T : TargetI>(
|
data class FollowForView<T : TargetI>(
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val createdBy: CitizenCreator,
|
override val createdBy: CitizenCreator,
|
||||||
override var target: T
|
override var target: T
|
||||||
) : ExtraI<T, CitizenRef>,
|
) : ExtraI<T, CitizenI>,
|
||||||
FollowRef(id),
|
FollowRef(id),
|
||||||
Created<CitizenRef> by Created.Imp(createdBy)
|
Created<CitizenI> by Created.Imp(createdBy)
|
||||||
|
|
||||||
class FollowForUpdate<T : TargetI, C : CitizenI>(
|
data class FollowForUpdate<T : TargetI, C : CitizenI>(
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val target: T,
|
override val target: T,
|
||||||
override val createdBy: C
|
override val createdBy: C
|
||||||
) : FollowRef(id),
|
) : FollowRef(id),
|
||||||
@@ -31,4 +30,4 @@ open class FollowRef(
|
|||||||
override val id: UUID
|
override val id: UUID
|
||||||
) : FollowI
|
) : FollowI
|
||||||
|
|
||||||
interface FollowI : EntityI
|
sealed interface FollowI : EntityI
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ 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
|
||||||
@@ -72,21 +74,24 @@ 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 nextPage = 1
|
var lastId: UUID? = null
|
||||||
do {
|
while (true) {
|
||||||
val paginate = findFollowsByTarget(target, nextPage, bulkSize)
|
val result = findFollowsByTarget(target, lastId, bulkSize)
|
||||||
paginate.result.forEach {
|
if (result.count() == 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result.forEach {
|
||||||
emit(it)
|
emit(it)
|
||||||
}
|
}
|
||||||
nextPage = paginate.currentPage + 1
|
lastId = result.last().id
|
||||||
} while (!paginate.isLastPage())
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun findFollowsByTarget(
|
abstract fun findFollowsByTarget(
|
||||||
target: Entity,
|
target: Entity,
|
||||||
page: Int = 1,
|
lastId: UUID?,
|
||||||
limit: Int = 300
|
limit: Int = 300
|
||||||
): Paginated<FollowForView<IN>>
|
): List<FollowForView<IN>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
|
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
|
||||||
@@ -107,14 +112,14 @@ class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRe
|
|||||||
|
|
||||||
override fun findFollowsByTarget(
|
override fun findFollowsByTarget(
|
||||||
target: Entity,
|
target: Entity,
|
||||||
page: Int,
|
lastId: UUID?,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Paginated<FollowForView<ArticleRef>> {
|
): List<FollowForView<ArticleRef>> {
|
||||||
return requester
|
return requester
|
||||||
.getFunction("find_follows_article_by_target")
|
.getFunction("find_follows_article_by_target")
|
||||||
.select(
|
.select(
|
||||||
page,
|
"start_id" to lastId,
|
||||||
limit,
|
"limit" to limit,
|
||||||
"target_id" to target.id
|
"target_id" to target.id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,9 +143,34 @@ class FollowConstitutionRepository(requester: Requester) : FollowRepository<Cons
|
|||||||
|
|
||||||
override fun findFollowsByTarget(
|
override fun findFollowsByTarget(
|
||||||
target: Entity,
|
target: Entity,
|
||||||
page: Int,
|
lastId: UUID?,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Paginated<FollowForView<ConstitutionRef>> {
|
): List<FollowForView<ConstitutionRef>> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FollowCitizenRepository(requester: Requester) : FollowRepository<CitizenRef, Citizen>(requester) {
|
||||||
|
override fun findByCitizen(
|
||||||
|
citizenId: UUID,
|
||||||
|
page: Int,
|
||||||
|
limit: Int
|
||||||
|
): Paginated<FollowForView<Citizen>> {
|
||||||
|
return requester.run {
|
||||||
|
getFunction("find_follows_citizen_by_citizen")
|
||||||
|
.select(
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
"created_by_id" to citizenId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findFollowsByTarget(
|
||||||
|
target: Entity,
|
||||||
|
lastId: UUID?,
|
||||||
|
limit: Int
|
||||||
|
): List<FollowForView<CitizenRef>> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ object FollowArticle {
|
|||||||
post<ArticleFollowRequest> {
|
post<ArticleFollowRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
|
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
|
||||||
ac.assert { canCreate(follow, citizenOrNull) }
|
ac.canCreate(follow, citizenOrNull).assert()
|
||||||
repo.follow(follow)
|
repo.follow(follow)
|
||||||
call.respond(HttpStatusCode.Created)
|
call.respond(HttpStatusCode.Created)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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
|
||||||
@@ -14,7 +15,6 @@ import io.ktor.locations.Location
|
|||||||
import io.ktor.locations.get
|
import io.ktor.locations.get
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import org.joda.time.DateTime
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
@@ -27,22 +27,10 @@ object GetFollowArticle {
|
|||||||
fun Route.getFollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
|
fun Route.getFollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
|
||||||
get<ArticleFollowRequest> {
|
get<ArticleFollowRequest> {
|
||||||
repo.findFollow(citizen, it.article)?.let { follow ->
|
repo.findFollow(citizen, it.article)?.let { follow ->
|
||||||
ac.assert { canView(follow, citizenOrNull) }
|
ac.canView(follow, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
follow.let { f ->
|
follow.toOutput()
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ object GetMyFollowsArticle {
|
|||||||
get<CitizenFollowArticleRequest> {
|
get<CitizenFollowArticleRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val follows = repo.findByCitizen(it.citizen)
|
val follows = repo.findByCitizen(it.citizen)
|
||||||
ac.assert { canView(follows.result, citizenOrNull) }
|
ac.canView(follows.result, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
follows.toOutput { f ->
|
follows.toOutput { f ->
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ object UnfollowArticle {
|
|||||||
delete<ArticleFollowRequest> {
|
delete<ArticleFollowRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
|
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
|
||||||
ac.assert { canDelete(follow, citizenOrNull) }
|
ac.canDelete(follow, citizenOrNull).assert()
|
||||||
repo.unfollow(follow)
|
repo.unfollow(follow)
|
||||||
call.respond(HttpStatusCode.NoContent)
|
call.respond(HttpStatusCode.NoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package fr.dcproject.component.follow.routes.citizen
|
||||||
|
|
||||||
|
import fr.dcproject.common.security.assert
|
||||||
|
import fr.dcproject.component.auth.citizen
|
||||||
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
|
import fr.dcproject.component.auth.mustBeAuth
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenRef
|
||||||
|
import fr.dcproject.component.follow.FollowAccessControl
|
||||||
|
import fr.dcproject.component.follow.database.FollowCitizenRepository
|
||||||
|
import fr.dcproject.component.follow.database.FollowForUpdate
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
|
import io.ktor.locations.Location
|
||||||
|
import io.ktor.locations.post
|
||||||
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@KtorExperimentalLocationsAPI
|
||||||
|
object FollowCitizen {
|
||||||
|
@Location("/citizens/{citizen}/follows")
|
||||||
|
class CitizenFollowRequest(citizen: UUID) {
|
||||||
|
val citizen = CitizenRef(citizen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.followCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
|
||||||
|
post<CitizenFollowRequest> {
|
||||||
|
mustBeAuth()
|
||||||
|
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
|
||||||
|
ac.canCreate(follow, citizenOrNull).assert()
|
||||||
|
repo.follow(follow)
|
||||||
|
call.respond(HttpStatusCode.Created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package fr.dcproject.component.follow.routes.citizen
|
||||||
|
|
||||||
|
import fr.dcproject.common.response.toOutput
|
||||||
|
import fr.dcproject.common.security.assert
|
||||||
|
import fr.dcproject.component.auth.citizen
|
||||||
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenRef
|
||||||
|
import fr.dcproject.component.follow.FollowAccessControl
|
||||||
|
import fr.dcproject.component.follow.database.FollowCitizenRepository
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
|
import io.ktor.locations.Location
|
||||||
|
import io.ktor.locations.get
|
||||||
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@KtorExperimentalLocationsAPI
|
||||||
|
object GetFollowCitizen {
|
||||||
|
@Location("/citizens/{citizen}/follows")
|
||||||
|
class CitizenFollowRequest(citizen: UUID) {
|
||||||
|
val citizen = CitizenRef(citizen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.getFollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
|
||||||
|
get<CitizenFollowRequest> {
|
||||||
|
repo.findFollow(citizen, it.citizen)?.let { follow ->
|
||||||
|
ac.canView(follow, citizenOrNull).assert()
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.OK,
|
||||||
|
follow.toOutput()
|
||||||
|
)
|
||||||
|
} ?: call.respond(HttpStatusCode.NoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package fr.dcproject.component.follow.routes.citizen
|
||||||
|
|
||||||
|
import fr.dcproject.common.response.toOutput
|
||||||
|
import fr.dcproject.common.security.assert
|
||||||
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
|
import fr.dcproject.component.auth.mustBeAuth
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenRef
|
||||||
|
import fr.dcproject.component.follow.FollowAccessControl
|
||||||
|
import fr.dcproject.component.follow.database.FollowCitizenRepository
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
|
import io.ktor.locations.Location
|
||||||
|
import io.ktor.locations.get
|
||||||
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@KtorExperimentalLocationsAPI
|
||||||
|
object GetMyFollowsCitizen {
|
||||||
|
@Location("/citizens/{citizen}/follows/citizens")
|
||||||
|
class CitizenFollowCitizenRequest(citizen: UUID) {
|
||||||
|
val citizen = CitizenRef(citizen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.getMyFollowsCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
|
||||||
|
get<CitizenFollowCitizenRequest> {
|
||||||
|
mustBeAuth()
|
||||||
|
val follows = repo.findByCitizen(it.citizen)
|
||||||
|
ac.canView(follows.result, citizenOrNull).assert()
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.OK,
|
||||||
|
follows.toOutput { f ->
|
||||||
|
object {
|
||||||
|
val id: UUID = f.id
|
||||||
|
val createdBy: Any = f.createdBy.toOutput()
|
||||||
|
val target: Any = f.target.let { t ->
|
||||||
|
object {
|
||||||
|
val id: UUID = t.id
|
||||||
|
val reference: String = f.target.reference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val createdAt: DateTime = f.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package fr.dcproject.component.follow.routes.citizen
|
||||||
|
|
||||||
|
import fr.dcproject.common.security.assert
|
||||||
|
import fr.dcproject.component.auth.citizen
|
||||||
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
|
import fr.dcproject.component.auth.mustBeAuth
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenRef
|
||||||
|
import fr.dcproject.component.follow.FollowAccessControl
|
||||||
|
import fr.dcproject.component.follow.database.FollowCitizenRepository
|
||||||
|
import fr.dcproject.component.follow.database.FollowForUpdate
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
|
import io.ktor.locations.Location
|
||||||
|
import io.ktor.locations.delete
|
||||||
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@KtorExperimentalLocationsAPI
|
||||||
|
object UnfollowCitizen {
|
||||||
|
@Location("/citizens/{citizen}/follows")
|
||||||
|
class CitizenFollowRequest(citizen: UUID) {
|
||||||
|
val citizen = CitizenRef(citizen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.unfollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
|
||||||
|
delete<CitizenFollowRequest> {
|
||||||
|
mustBeAuth()
|
||||||
|
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
|
||||||
|
ac.canDelete(follow, citizenOrNull).assert()
|
||||||
|
repo.unfollow(follow)
|
||||||
|
call.respond(HttpStatusCode.NoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ object FollowConstitution {
|
|||||||
post<ConstitutionFollowRequest> {
|
post<ConstitutionFollowRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
|
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
|
||||||
ac.assert { canCreate(follow, citizenOrNull) }
|
ac.canCreate(follow, citizenOrNull).assert()
|
||||||
repo.follow(follow)
|
repo.follow(follow)
|
||||||
call.respond(HttpStatusCode.Created)
|
call.respond(HttpStatusCode.Created)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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
|
||||||
@@ -14,7 +15,6 @@ import io.ktor.locations.Location
|
|||||||
import io.ktor.locations.get
|
import io.ktor.locations.get
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import org.joda.time.DateTime
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
@@ -27,22 +27,10 @@ object GetFollowConstitution {
|
|||||||
fun Route.getFollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
|
fun Route.getFollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
|
||||||
get<ConstitutionFollowRequest> {
|
get<ConstitutionFollowRequest> {
|
||||||
repo.findFollow(citizen, it.constitution)?.let { follow ->
|
repo.findFollow(citizen, it.constitution)?.let { follow ->
|
||||||
ac.assert { canView(follow, citizenOrNull) }
|
ac.canView(follow, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
follow.let { f ->
|
follow.toOutput()
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ object GetMyFollowsConstitution {
|
|||||||
get<CitizenFollowConstitutionRequest> {
|
get<CitizenFollowConstitutionRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val follows = repo.findByCitizen(it.citizen)
|
val follows = repo.findByCitizen(it.citizen)
|
||||||
ac.assert { canView(follows.result, citizenOrNull) }
|
ac.canView(follows.result, citizenOrNull).assert()
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
follows.toOutput { f ->
|
follows.toOutput { f ->
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ object UnfollowConstitution {
|
|||||||
delete<ConstitutionUnfollowRequest> {
|
delete<ConstitutionUnfollowRequest> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
|
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
|
||||||
ac.assert { canDelete(follow, citizenOrNull) }
|
ac.canDelete(follow, citizenOrNull).assert()
|
||||||
repo.unfollow(follow)
|
repo.unfollow(follow)
|
||||||
call.respond(HttpStatusCode.NoContent)
|
call.respond(HttpStatusCode.NoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
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
|
||||||
@@ -12,7 +14,11 @@ 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
|
||||||
|
|
||||||
open class Notification(
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
|
||||||
|
@JsonSubTypes(
|
||||||
|
JsonSubTypes.Type(value = ArticleUpdateNotificationMessage::class, name = "article")
|
||||||
|
)
|
||||||
|
open class NotificationMessage(
|
||||||
val type: String,
|
val type: String,
|
||||||
val createdAt: DateTime = DateTime.now()
|
val createdAt: DateTime = DateTime.now()
|
||||||
) {
|
) {
|
||||||
@@ -42,16 +48,16 @@ open class Notification(
|
|||||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Notification> fromString(raw: String): T = mapper.readValue(raw)
|
inline fun <reified T : NotificationMessage> fromString(raw: String): T = mapper.readValue(raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class EntityNotification(
|
open class EntityNotificationMessage <E : Entity> (
|
||||||
val target: Entity,
|
open val target: E,
|
||||||
type: String,
|
type: String,
|
||||||
val action: String
|
val action: String
|
||||||
) : Notification(type)
|
) : NotificationMessage(type)
|
||||||
|
|
||||||
class ArticleUpdateNotification(
|
data class ArticleUpdateNotificationMessage(
|
||||||
target: ArticleForView
|
override val target: ArticleForView
|
||||||
) : EntityNotification(target, "article", "update")
|
) : EntityNotificationMessage<ArticleForView>(target, "article", "update")
|
||||||
@@ -7,12 +7,15 @@ import kotlinx.coroutines.coroutineScope
|
|||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class Publisher(
|
class NotificationPublisherAsync(
|
||||||
private val factory: ConnectionFactory,
|
private val factory: ConnectionFactory,
|
||||||
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName),
|
private val logger: Logger = LoggerFactory.getLogger(NotificationPublisherAsync::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 ->
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package fr.dcproject.component.notification.email.content
|
||||||
|
|
||||||
|
import fr.dcproject.component.article.database.ArticleWithTitleI
|
||||||
|
import fr.dcproject.component.citizen.database.Citizen
|
||||||
|
|
||||||
|
data class ArticleNotificationEmailContent(
|
||||||
|
private val citizen: Citizen,
|
||||||
|
private val target: ArticleWithTitleI,
|
||||||
|
private val domain: String,
|
||||||
|
) : NotificationEmailContent {
|
||||||
|
override val subject: String
|
||||||
|
get() = """New version for article "${target.title}""""
|
||||||
|
|
||||||
|
override val contentHtml
|
||||||
|
get() = run {
|
||||||
|
"""
|
||||||
|
Hello ${citizen.name.getFullName()},<br/>
|
||||||
|
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val content
|
||||||
|
get() = run {
|
||||||
|
"""
|
||||||
|
Hello ${citizen.name.getFullName()},
|
||||||
|
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package fr.dcproject.component.notification.email.content
|
||||||
|
|
||||||
|
import fr.dcproject.component.citizen.database.Citizen
|
||||||
|
|
||||||
|
data class CitizenNotificationEmailContent(
|
||||||
|
private val citizen: Citizen,
|
||||||
|
private val target: Citizen,
|
||||||
|
private val domain: String,
|
||||||
|
) : NotificationEmailContent {
|
||||||
|
override val subject: String
|
||||||
|
get() = """New activity for the citizen "${target.name}""""
|
||||||
|
|
||||||
|
override val contentHtml
|
||||||
|
get() = run {
|
||||||
|
"""
|
||||||
|
Hello ${citizen.name.getFullName()},
|
||||||
|
The citizen "${target.name}" was new activity, check it here: <a href="http://$domain/citizens/${target.id}">here</a>
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val content
|
||||||
|
get() = run {
|
||||||
|
"""
|
||||||
|
Hello ${citizen.name.getFullName()},
|
||||||
|
The citizen "${target.name}" was new activity, check it here: http://$domain/citizens/${target.id}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package fr.dcproject.component.notification.email.content
|
||||||
|
|
||||||
|
interface NotificationEmailContent {
|
||||||
|
val subject: String
|
||||||
|
val content: String
|
||||||
|
val contentHtml: String
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package fr.dcproject.component.notification
|
package fr.dcproject.component.notification.push
|
||||||
|
|
||||||
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
|
||||||
@@ -28,31 +29,42 @@ 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<Notification>,
|
incoming: Flow<NotificationMessage>,
|
||||||
onRecieve: suspend (Notification) -> Unit,
|
onReceive: suspend (NotificationMessage) -> Unit,
|
||||||
) {
|
) {
|
||||||
class Builder(val redisClient: RedisClient) {
|
class Builder(redisClient: RedisClient) {
|
||||||
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
|
private val redisConnection = redisClient.connect()
|
||||||
private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis PubSub")
|
private val redisConnectionPubSub = redisClient.connectPubSub()
|
||||||
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis Async")
|
private val redis: RedisAsyncCommands<String, String> = redisConnection.async()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Listener with citizen, incoming flow and set an outgoing callback
|
||||||
|
*/
|
||||||
fun build(
|
fun build(
|
||||||
citizen: CitizenI,
|
citizen: CitizenI,
|
||||||
incoming: Flow<Notification>,
|
incoming: Flow<NotificationMessage>,
|
||||||
onRecieve: suspend (Notification) -> Unit,
|
onReceive: suspend (NotificationMessage) -> Unit,
|
||||||
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve)
|
): NotificationPushListener = NotificationPushListener(redis, redisConnectionPubSub, citizen, incoming, onReceive)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build NotificationPush with only a WebSocket session
|
||||||
|
*/
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
|
fun build(ws: DefaultWebSocketServerSession): NotificationPushListener {
|
||||||
/* 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<Notification> = ws.incoming.consumeAsFlow()
|
val incomingFlow: Flow<NotificationMessage> = ws.incoming.consumeAsFlow()
|
||||||
.mapNotNull<Frame, Text> { it as? Frame.Text }
|
.mapNotNull<Frame, Text> { it as? Text }
|
||||||
.map { it.readText() }
|
.map { it.readText() }
|
||||||
.map { Notification.fromString(it) }
|
.map { NotificationMessage.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()))
|
||||||
@@ -62,69 +74,100 @@ class NotificationsPush private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
getNotifications().collect {
|
getNewUnreadNotifications().collect {
|
||||||
onRecieve(it)
|
onReceive(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 {
|
||||||
markAsRead(it)
|
it.markAsRead()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get old notification and sent it to websocket */
|
/* Get old notification and sent it to websocket */
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getNotifications().collect { onRecieve(it) }
|
getNewUnreadNotifications().collect {
|
||||||
|
onReceive(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lisen redis event, and sent the new notification into websocket */
|
/* Listen 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") ?: error("Unable to subscribe to redis events")
|
async()?.psubscribe("__key*__:$key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the redis subscription
|
||||||
|
*/
|
||||||
fun close() {
|
fun close() {
|
||||||
redisConnectionPubSub.removeListener(listener)
|
redisConnectionPubSub.removeListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Return flow with all new notifications */
|
/**
|
||||||
private fun getNotifications() = flow<Notification> {
|
* Get All new notification from redis and
|
||||||
|
* Return flow with notifications
|
||||||
|
*
|
||||||
|
* On start, on the first call, this method return all unread notification of the user
|
||||||
|
*
|
||||||
|
* Internally this method return all messages that greater of the lastScore,
|
||||||
|
* then define the lastScore with the score of the last message.
|
||||||
|
*/
|
||||||
|
private fun getNewUnreadNotifications() = flow<NotificationMessage> {
|
||||||
redis
|
redis
|
||||||
.zrangebyscoreWithScores(
|
.zrangebyscoreWithScores(
|
||||||
key,
|
key,
|
||||||
Range.from(
|
Range.from(
|
||||||
Boundary.excluding(score),
|
Boundary.excluding(lastScore),
|
||||||
Boundary.including(Double.POSITIVE_INFINITY)
|
Boundary.including(Double.POSITIVE_INFINITY)
|
||||||
),
|
),
|
||||||
Limit.from(100)
|
Limit.from(100)
|
||||||
)
|
)
|
||||||
.get().forEach {
|
.get().forEach {
|
||||||
emit(Notification.fromString(it.value))
|
/* Build message object from raw string and return it */
|
||||||
if (it.score > score) score = it.score
|
emit(NotificationMessage.fromString(it.value))
|
||||||
|
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(notificationMessage.id),
|
Boundary.including(id),
|
||||||
Boundary.including(notificationMessage.id)
|
Boundary.including(id)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: JsonProcessingException) {
|
} catch (e: JsonProcessingException) {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package fr.dcproject.component.notification.routes
|
package fr.dcproject.component.notification.routes
|
||||||
|
|
||||||
import fr.dcproject.component.notification.NotificationsPush
|
import fr.dcproject.component.notification.push.NotificationPushListener
|
||||||
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(pushBuilder: NotificationsPush.Builder) {
|
fun Route.notificationArticle(pushListenerBuilder: NotificationPushListener.Builder) {
|
||||||
webSocket("/notifications") {
|
webSocket("/notifications") {
|
||||||
pushBuilder.build(this)
|
pushListenerBuilder.build(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import fr.dcproject.component.citizen.database.CitizenI
|
|||||||
import fr.dcproject.component.citizen.database.CitizenRef
|
import fr.dcproject.component.citizen.database.CitizenRef
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
open class Opinion<T : TargetI>(
|
data class Opinion<T : TargetI>(
|
||||||
id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
override val createdBy: CitizenCreator,
|
override val createdBy: CitizenCreator,
|
||||||
override val target: T,
|
override val target: T,
|
||||||
val choice: OpinionChoice
|
val choice: OpinionChoice
|
||||||
@@ -39,4 +39,4 @@ open class OpinionRef(
|
|||||||
override val id: UUID
|
override val id: UUID
|
||||||
) : OpinionI, TargetRef(id)
|
) : OpinionI, TargetRef(id)
|
||||||
|
|
||||||
interface OpinionI : EntityI
|
sealed interface OpinionI : EntityI
|
||||||
|
|||||||
@@ -7,6 +7,4 @@ interface Opinionable {
|
|||||||
val opinions: Opinions
|
val opinions: Opinions
|
||||||
}
|
}
|
||||||
|
|
||||||
class OpinionableImp : Opinionable {
|
data class OpinionableImp(override var opinions: OpinionsMutable = mutableMapOf()) : Opinionable
|
||||||
override var opinions: OpinionsMutable = mutableMapOf()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ object GetCitizenOpinions {
|
|||||||
get<CitizenOpinions> {
|
get<CitizenOpinions> {
|
||||||
mustBeAuth()
|
mustBeAuth()
|
||||||
val opinionsEntities: List<Opinion<ArticleRef>> = repo.findCitizenOpinionsByTargets(it.citizen, it.id)
|
val opinionsEntities: List<Opinion<ArticleRef>> = repo.findCitizenOpinionsByTargets(it.citizen, it.id)
|
||||||
ac.assert { canView(opinionsEntities, citizenOrNull) }
|
ac.canView(opinionsEntities, citizenOrNull).assert()
|
||||||
|
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user