117 Commits

Author SHA1 Message Date
03350db56f change ac.assert { can*() } to as.can*().assert() 2022-03-03 19:46:49 +01:00
f380231e1e Multiple minors fix and improve
use data class for entity
Add sealed on entity interfaces
Replace listOf() by setOf() instead of deduplicate
use interface instead of EntityRef
replace .toLowerCase() to .lowercase()
fix test.sh
2022-03-03 19:42:18 +01:00
1c013e3e15 lint 2022-02-25 23:48:33 +01:00
003aa9ff05 update gradle plugin 2022-02-25 23:48:33 +01:00
44c5f1b15b Merge pull request #104 from flecomte/sibling
Rename findVersionsByVersionId to findSiblingVersions
2022-02-25 23:45:45 +01:00
2f3a2f9b8e Rename findVersionsByVersionId to findSiblingVersions 2022-02-25 23:17:03 +01:00
1a62c5ec9a Merge pull request #103 from flecomte/update_gradle2
gradle dependencies fix
2022-02-25 22:55:12 +01:00
55674cbef1 upgrade jacoco 2022-02-25 22:44:38 +01:00
ac5dbbc384 update gradle to 7.4 2022-02-25 22:44:26 +01:00
9460992f37 update gradle to 7.2 2022-02-25 22:43:49 +01:00
8013cfb266 gradle dependencies fix 2022-02-25 22:42:32 +01:00
f577ea70b3 Merge pull request #96 from flecomte/60
#60 Can follow citizen
2021-04-27 21:48:16 +02:00
2673cf527a Clean useless error 2021-04-27 21:30:06 +02:00
371483ccde Improve query findFollowsByTarget & add tests 2021-04-27 18:52:36 +02:00
76e4033a22 deduplicate follow output 2021-04-18 02:36:14 +02:00
fee5e5784b Refactoring of Notification system 2021-04-18 02:16:36 +02:00
1c33c026f0 #60 Can follow citizen 2021-04-17 01:37:32 +02:00
4871e7d780 Merge pull request #95 from flecomte/61
#61 Fix version date returned for the article.createdAt
2021-04-16 21:44:03 +02:00
359450ad8f #61 Fix version date returned for the article.createdAt 2021-04-16 21:23:43 +02:00
11903a4cda Merge pull request #94 from flecomte/62
#62 if not connected, you not must view the articles draft
2021-04-16 18:30:15 +02:00
1a8b544cdb #62 if not connected, you not must view the articles draft 2021-04-16 18:15:22 +02:00
e2c1f15ab8 Merge pull request #93 from flecomte/dependencies
Use dynamic dependencies
2021-04-16 17:25:52 +02:00
dc87c95bb4 Enable zip64 for jar 2021-04-16 16:59:13 +02:00
d4cc3f21da fix CI 2021-04-16 16:19:04 +02:00
59c050d14d Update dependencies
Fix for koin 3.0
2021-04-16 16:09:59 +02:00
cb9bd0c14b Use gradle Lock file 2021-04-16 16:09:58 +02:00
1ce3a18d8e Update gradle plugins versions 2021-04-16 16:09:58 +02:00
be6fb8dc12 Update gradle to 7.0 2021-04-16 16:09:58 +02:00
cd9fd569d7 launch CI on master 2021-04-16 03:40:39 +02:00
2920186352 Merge pull request #91 from flecomte/21-valid-input
Valider les resource entrente
2021-04-16 03:27:10 +02:00
cccabb2cc9 improve sonarqube action 2021-04-16 03:17:38 +02:00
620cd73fec Change intellij tasks 2021-04-16 03:09:56 +02:00
e474a40068 Change tests task and CI 2021-04-16 03:09:55 +02:00
242bf9c9b3 Fix commentSqlTest 2021-04-16 02:53:05 +02:00
543f3fb9bb Add retry for viewsTest 2021-04-16 02:53:04 +02:00
994e266b52 Add validation on route CreateWorkgroup 2021-04-16 00:08:57 +02:00
3f392eece6 Add validation on route EditWorkgroup 2021-04-15 23:45:47 +02:00
518b59e9aa Add validation on route GetWorkgroups 2021-04-15 22:20:16 +02:00
596b7ff0c9 Extract Workgroup Member test into external file 2021-04-15 20:52:34 +02:00
87175eb8ea clean route comment 2021-04-15 02:53:32 +02:00
1118866856 Add validation on route PutVoteOnConstitution 2021-04-15 02:42:29 +02:00
367f59ee18 Add validation on route PutVoteOnComment 2021-04-15 02:33:46 +02:00
0588f88f9a Add validation on route PutVoteOnArticle 2021-04-15 02:20:57 +02:00
496cf50d88 Add validation on route GetCitizenVotesOnArticle 2021-04-15 01:27:48 +02:00
13253e4af1 Add validation on route GetMyOpinionsArticle 2021-04-15 01:06:46 +02:00
39c665b7a9 Add Test for Notification routes
Add @JsonSubTypes on Notification
return all creator on request find_follows_article_by_target
Add testNotifications task
2021-04-14 23:51:44 +02:00
50b4cf1816 Add testFollow task 2021-04-12 01:03:57 +02:00
6a5e00bb4d Add validation on Constitution routes 2021-04-11 00:54:09 +02:00
0c8bcbd634 Add limit on content field (comment request) 2021-04-11 00:54:08 +02:00
8223dd21bb Add validation on route CreateComments & EditComment
rename POST /comments/{comment}/children
method edit and create comment of repository return edited/created comment
2021-04-10 01:16:09 +02:00
27e405c585 Move tests 2021-04-09 18:43:59 +02:00
34513e25b6 Add validation on route CreateConstitutionComment & GetConstitutionCommentRequest 2021-04-09 18:39:03 +02:00
f5c1aa29e8 Add validation on route GetArticleComments 2021-04-09 18:06:32 +02:00
875d0bfffa Add test 404 for GetArticle route 2021-04-09 16:43:46 +02:00
fb7b07340a Improve test of password validation 2021-04-09 01:09:09 +02:00
a07b19a3cb Add validation on route CreateCommentArticle 2021-04-09 00:58:35 +02:00
13cdaaf01a Add validation on route FindCitizens 2021-04-08 22:25:43 +02:00
e473e62068 remove CodeFactor & Codacy 2021-04-08 18:08:48 +02:00
9d3eeeb04b Add validation on route ChangePasswordCitizenRequest 2021-04-08 18:02:27 +02:00
eb399392c9 remove parallel run for tests 2021-04-08 03:03:04 +02:00
1ec1c59c8c remove useless log 2021-04-08 03:02:03 +02:00
9511331cd2 Add validation on route Register 2021-04-08 02:10:45 +02:00
33a8cdb169 Add email validation 2021-04-08 02:02:46 +02:00
6aa3ddb28d Add Password validation 2021-04-08 01:55:10 +02:00
708d241a26 Add tags on tests 2021-04-07 20:53:51 +02:00
e4745e71c2 Add validation on route UpsertArticle 2021-04-07 20:53:21 +02:00
e26710898e add example on openapi 400 error 2021-04-06 23:35:36 +02:00
fe11384ad2 Add validation on route GetOneArticle 2021-04-06 23:04:02 +02:00
61a7091736 Add validation on route Article versions 2021-04-06 00:36:08 +02:00
2ef9f65f2c Clean BadRequest response 2021-04-05 01:40:12 +02:00
b5fc3d25bb Improve Article validation & test on BadRequest 2021-04-05 00:50:02 +02:00
3faf2e5f0d Add function to respond on BadRequest 2021-04-05 00:48:58 +02:00
ab418ae300 Add openapi response of error 400 2021-04-05 00:48:11 +02:00
395d64a44a create testArticle gradle task 2021-04-04 21:02:02 +02:00
a300e275d4 Valid FindArticles request with Konform 2021-04-04 21:02:02 +02:00
3a18ef0554 Improve articles request test 2021-04-04 21:01:57 +02:00
921a545877 Merge pull request #92 from flecomte/sonarq
Sonarcloud
2021-04-04 21:01:27 +02:00
ef942b956e Use sonarcloud 2021-04-04 01:35:02 +02:00
ff74ad7e47 Merge pull request #90 from flecomte/improve-test
Improve tests
2021-04-03 00:39:16 +02:00
2bb90ced03 Refactor 'the response should contain list' 2021-04-03 00:31:24 +02:00
a48cd52652 Add Tags on tests 2021-04-03 00:10:01 +02:00
dd4c2dadab Fix parameters schema validation 2021-04-02 23:47:20 +02:00
c81b63aef2 Merge pull request #89 from flecomte/ArticleViewManager
ArticleViewRepository
2021-04-02 12:39:34 +02:00
cb762a446a Move ArticleViewRepository 2021-04-02 12:29:50 +02:00
db810ab0c6 Rename ArticleViewManager to ArticleViewRepository 2021-04-02 12:29:11 +02:00
01c5b78325 Merge pull request #87 from flecomte/jwt-token-into-env
move JWT secret into ENV
2021-03-31 18:23:13 +02:00
1bc7293660 move JWT secret into ENV 2021-03-31 17:58:47 +02:00
55c890aca5 Merge pull request #86
move "Check auth on all routes" extension into the class
2021-03-31 12:31:51 +02:00
c0e364637a move "Check auth on all routes" extension into the class 2021-03-31 12:30:37 +02:00
0a1ed9ba82 Merge pull request #85 from flecomte/optimise-testsql
Opimize testSql
2021-03-31 03:09:12 +02:00
620085fda8 Optimise gradle task TestSql 2021-03-31 02:58:37 +02:00
3b5c1cf68a Merge pull request #84 from flecomte/69
Error codes
2021-03-31 02:53:58 +02:00
a0d07e88a1 Fix all security routes 2021-03-31 02:43:43 +02:00
f17277c0e9 Test all security of routes #76 2021-03-31 02:35:59 +02:00
9f13213a35 Fix error text when openapi definition was not found 2021-03-27 21:57:53 +01:00
5f0b8de159 #69 Format HTTP error
add 403 for /articles route
2021-03-26 01:53:41 +01:00
6b66130ddc #69 Move HttpStatusPage catch 2021-03-25 23:40:05 +01:00
7f93ec5044 Merge pull request #82 from flecomte/lint
Optimize CI
2021-03-25 02:07:52 +01:00
1be608e6b2 Add Codacy badge 2021-03-25 02:05:05 +01:00
b13cd5544c add coveralls on CI 2021-03-25 01:53:57 +01:00
104f0fb3fc remove distZip & distTar 2021-03-24 23:06:38 +01:00
b2f40ff421 Restrict CI on pull_request on master 2021-03-24 21:56:58 +01:00
09e81620a1 rollback lintCheck after test, create task testAll 2021-03-24 21:32:28 +01:00
7e16c7bb74 Merge pull request #81
Lint
2021-03-24 19:49:34 +01:00
fe953fc967 lintCheck after test 2021-03-24 19:48:14 +01:00
453fd2225c Merge pull request #80
rename openapi file
2021-03-24 19:39:23 +01:00
70fd54d831 Fix destination installation doc 2021-03-24 19:35:11 +01:00
dcf7a2bc06 Rename openapi file 2021-03-24 19:34:39 +01:00
118af0170a Remove unused openapi file 2021-03-24 19:34:00 +01:00
0aa8089a9a Merge pull request #79
Add codefactor badge
2021-03-24 19:29:44 +01:00
fef5f3b396 CodeFactor badge 2021-03-24 19:27:09 +01:00
1838b90ac9 Merge pull request #78
Create CI
2021-03-24 19:12:41 +01:00
73fa2be91f Split CI task test 2021-03-24 19:11:04 +01:00
52183abd08 Lint 2021-03-24 19:08:41 +01:00
e19266d4cc Create CI Action 2021-03-24 19:07:02 +01:00
f458d7b674 Move SQL test files 2021-03-24 19:07:01 +01:00
29d4d6ec25 Merge pull request #77 from flecomte/refactoring-component-and-immutable
Big refactoring
2021-03-24 19:06:06 +01:00
210 changed files with 6335 additions and 4710 deletions

160
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,160 @@
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: Tests
on:
push:
branches:
- 'master'
pull_request:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt
- name: processResources
run: gradle processResources
- name: processTestResources
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: processResources
- uses: actions/upload-artifact@v2
with:
name: Build
path: build
testSql:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/download-artifact@v2
with:
name: Build
path: build
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testSqlComposeUp
- name: TestSql
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testSql
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/download-artifact@v2
with:
name: Build
path: build
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testComposeUp
- name: Test
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: test
- name: Coverage
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
- name: Cache SonarCloud packages
uses: actions/cache@v1
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Test
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: test
- name: Build and analyze
uses: gradle/gradle-build-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
gradle-version: '7.4'
arguments: sonarqube --info
lint:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/download-artifact@v2
with:
name: Build
path: build
- name: Lint
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: ktlintCheck

View File

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

2
.idea/dataSources.xml generated
View File

@@ -11,7 +11,7 @@
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5433/test</jdbc-url>
<jdbc-url>jdbc:postgresql://localhost:15432/test</jdbc-url>
</data-source>
<data-source source="LOCAL" name="sonar@localhost" uuid="ee78beab-120d-4740-ad21-d4d9e2121d25">
<driver-ref>postgresql</driver-ref>

View File

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

View File

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

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

View File

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

View File

@@ -1,6 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="SONAR_TOKEN" value="15ad34f46763706727d884ced12c48d5222fe639" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />

View File

@@ -1,5 +1,5 @@
<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>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />

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

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

View File

@@ -1,6 +1,14 @@
# DC Project
[Installation](./doc/installation)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=dc-project)
[![Tests](https://github.com/flecomte/dc-project/actions/workflows/tests.yml/badge.svg)](https://github.com/flecomte/dc-project/actions/workflows/tests.yml)
[![Coverage Status](https://coveralls.io/repos/github/flecomte/dc-project/badge.svg?branch=master)](https://coveralls.io/github/flecomte/dc-project?branch=master)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=coverage)](https://sonarcloud.io/dashboard?id=dc-project)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=ncloc)](https://sonarcloud.io/dashboard?id=dc-project)
[Installation](./doc/installation/Installation.md)
### Run dockers
```bash

View File

@@ -9,12 +9,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.owasp.dependencycheck.reporting.ReportGenerator
import org.slf4j.LoggerFactory
val ktorVersion = "1.5.0"
val kotlinVersion = "1.4.30"
val coroutinesVersion = "1.4.3"
val ktorVersion = "1.5.4"
val kotlinVersion = "1.5.31"
val coroutinesVersion = "1.5.2"
val logbackVersion = "1.2.3"
val koinVersion = "2.0.1"
val jacksonVersion = "2.12.1"
val koinVersion = "3.1.5"
val jacksonVersion = "2.13.1"
group = "com.github.flecomte"
version = versioning.info.run {
@@ -28,19 +28,24 @@ version = versioning.info.run {
plugins {
jacoco
application
maven
`maven-publish`
id("maven-publish")
kotlin("jvm") version "1.4.30"
kotlin("plugin.serialization") version "1.4.30"
kotlin("jvm") version "1.5.31"
kotlin("plugin.serialization") version "1.5.31"
id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
id("org.owasp.dependencycheck") version "6.1.1"
id("org.sonarqube") version "3.1.1"
id("net.nemerosa.versioning") version "2.14.0"
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
id("com.avast.gradle.docker-compose") version "0.14.0"
id("com.github.johnrengelman.shadow") version "7.1.2"
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
id("org.owasp.dependencycheck") version "6.1.5"
id("org.sonarqube") version "3.3"
id("net.nemerosa.versioning") version "2.15.1"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.avast.gradle.docker-compose") version "0.15.1"
id("com.github.kt3k.coveralls") version "2.12.0"
}
dependencyLocking {
lockAllConfigurations()
// lockMode.set(LockMode.STRICT)
}
application {
@@ -55,11 +60,14 @@ buildscript {
maven { url = uri("https://jitpack.io") }
}
dependencies {
classpath("com.typesafe:config:1.4.1")
classpath("com.github.flecomte:postgres-json:2.1.1")
classpath("com.typesafe:config:1.4.2")
classpath("com.github.flecomte:postgres-json:2.1.2")
}
}
tasks.distZip.configure { enabled = false }
tasks.distTar.configure { enabled = false }
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
@@ -71,7 +79,7 @@ val migration by tasks.registering {
dependsOn(tasks.named("composeUp"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/../src/main/resources/application.conf")).resolve()
val config = ConfigFactory.parseFile(file("$buildDir/resources/main/application.conf")).resolve()
val connection = Connection(
host = config.getString("db.host"),
port = config.getInt("db.port"),
@@ -81,8 +89,8 @@ val migration by tasks.registering {
)
Migrations(
connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI()
file("$buildDir/resources/main/sql/migrations").toURI(),
file("$buildDir/resources/main/sql/functions").toURI()
).run {
run()
}
@@ -90,11 +98,11 @@ val migration by tasks.registering {
}
val migrationTest by tasks.registering {
group = "verification"
group = "tests"
dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/../src/test/resources/application-test.conf")).resolve()
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
val connection = Connection(
host = config.getString("db.host"),
port = config.getInt("db.port"),
@@ -104,8 +112,8 @@ val migrationTest by tasks.registering {
)
Migrations(
connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI()
file("$buildDir/resources/main/sql/migrations").toURI(),
file("$buildDir/resources/main/sql/functions").toURI()
).run {
run()
connection.disconnect()
@@ -114,12 +122,12 @@ val migrationTest by tasks.registering {
}
val testSql by tasks.registering {
group = "verification"
dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown"))
group = "tests"
dependsOn(tasks.named("processResources"))
dependsOn(tasks.named("processTestResources"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/../src/test/resources/application-test.conf")).resolve()
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
val connection = Connection(
host = config.getString("db.host"),
@@ -131,16 +139,14 @@ val testSql by tasks.registering {
Migrations(
connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI(),
file("$buildDir/../src/test/sql/fixtures").toURI()
).run {
run()
}
file("$buildDir/resources/main/sql/migrations").toURI(),
file("$buildDir/resources/main/sql/functions").toURI(),
file("$buildDir/resources/test/sql/fixtures").toURI()
).run()
Requester.RequesterFactory(
connection = connection,
queriesDirectory = file("$buildDir/../src/test/sql").toURI()
queriesDirectory = file("$buildDir/resources/test/sql").toURI()
).createRequester().run {
getQueries().map {
try {
@@ -163,6 +169,7 @@ tasks.withType<Jar> {
)
)
}
isZip64 = true
}
tasks.withType<KotlinCompile> {
@@ -176,9 +183,12 @@ tasks.withType<KotlinCompile> {
tasks.named<ShadowJar>("shadowJar") {
mergeServiceFiles("META-INF/services")
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}")
isZip64 = true
}
tasks.sonarqube.configure { dependsOn(tasks.jacocoTestReport) }
tasks.sonarqube.configure {
dependsOn(tasks.jacocoTestReport)
}
val sourcesJar by tasks.registering(Jar::class) {
group = "build"
@@ -190,10 +200,13 @@ tasks.test {
useJUnit()
useJUnitPlatform()
systemProperty("junit.jupiter.execution.parallel.enabled", true)
dependsOn(testSql)
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
}
coveralls {
sourceDirs.add("src/main/kotlin")
}
apply(plugin = "docker-compose")
dockerCompose {
projectName = "dc-project"
@@ -203,23 +216,20 @@ dockerCompose {
removeVolumes = false
removeContainers = false
isRequiredBy(project.tasks.run)
createNested("testSql").apply {
projectName = "dc-project_test"
useComposeFiles = listOf("docker-compose-test.yml")
startedServices = listOf("db", "elasticsearch")
stopContainers = false
}
createNested("test").apply {
projectName = "dc-project_test"
useComposeFiles = listOf("docker-compose-test.yml")
stopContainers = false
isRequiredBy(project.tasks.test)
isRequiredBy(project.tasks.named("testSql"))
}
createNested("sonarqube").apply {
projectName = "dc-project"
useComposeFiles = listOf("docker-compose-sonar.yml")
stopContainers = false
removeVolumes = false
removeContainers = false
// isRequiredBy(project.tasks.sonarqube)
}
}
tasks.sonarqube.configure { dependsOn(tasks.named("sonarqubeComposeUp")) }
publishing {
if (versioning.info.dirty == false) {
@@ -248,7 +258,7 @@ publishing {
}
jacoco {
toolVersion = "0.8.6"
toolVersion = "0.8.7"
applyTo(tasks.run.get())
}
@@ -267,6 +277,7 @@ tasks.jacocoTestReport {
detekt {
buildUponDefaultConfig = true // preconfigure defaults
ignoreFailures = true
// config = files("$projectDir/config/detekt.yml") // point to your custom config defining rules to run, overwriting default behavior
// baseline = file("$projectDir/config/baseline.xml") // a way of suppressing issues before introducing detekt
@@ -281,6 +292,7 @@ detekt {
tasks.withType<Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
this.jvmTarget = "11"
ignoreFailures = true
}
val setMaxMapCount = tasks.create<Exec>("setMaxMapCount") {
@@ -293,7 +305,81 @@ val setMaxMapCount = tasks.create<Exec>("setMaxMapCount") {
}
}
}
tasks.named("testComposeUp").configure { dependsOn(setMaxMapCount) }
tasks.named("testComposeUp").configure {
if (OperatingSystem.current().isWindows) {
dependsOn(setMaxMapCount)
}
}
tasks.register("testWithDependencies", Test::class) {
group = "tests"
dependsOn(tasks.named("testComposeUp"))
dependsOn(tasks.ktlintCheck)
dependsOn(testSql)
dependsOn(tasks.jacocoTestReport)
finalizedBy(tasks.sonarqube) // report is always generated after tests run
}
tasks.register("testArticles", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("article")
}
}
tasks.register("testCitizens", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("citizen")
}
}
tasks.register("testComments", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("comment")
}
}
tasks.register("testConstitutions", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("constitution")
}
}
tasks.register("testFollows", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("follow")
}
}
tasks.register("testNotifications", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("notification")
}
}
tasks.register("testOpinions", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("opinion")
}
}
tasks.register("testVotes", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("vote")
}
}
tasks.register("testWorkgroups", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("workgroup")
}
}
tasks.register("testViews", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("view")
}
}
dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
@@ -302,7 +388,6 @@ dependencyCheck {
repositories {
mavenLocal()
jcenter()
maven { url = uri("https://kotlin.bintray.com/ktor") }
maven { url = uri("https://jitpack.io") }
}
@@ -320,31 +405,32 @@ dependencies {
implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$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("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion")
implementation("net.pearx.kasechange:kasechange-jvm:1.3.0")
implementation("com.auth0:java-jwt:3.12.0")
implementation("com.github.jasync-sql:jasync-postgresql:1.1.6")
implementation("com.github.flecomte:postgres-json:2.1.1")
implementation("com.github.flecomte:postgres-json:2.1.2")
implementation("com.sendgrid:sendgrid-java:4.7.1")
implementation("io.lettuce:lettuce-core:5.3.6.RELEASE") // TODO update to 6.0.2
implementation("com.rabbitmq:amqp-client:5.10.0")
implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
implementation("com.jayway.jsonpath:json-path:2.5.0")
implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.0")
implementation("io.konform:konform:0.3.0")
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion")
testImplementation("org.koin:koin-test:$koinVersion")
testImplementation("io.insert-koin:koin-test:$koinVersion")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
testImplementation("org.amshove.kluent:kluent:1.61")
testImplementation("io.mockk:mockk-agent-api:1.10.6")
testImplementation("io.mockk:mockk-agent-jvm:1.10.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
testImplementation("org.amshove.kluent:kluent:1.68")
testImplementation("io.mockk:mockk:1.12.2")
testImplementation("io.mockk:mockk-agent-api:1.12.2")
testImplementation("io.mockk:mockk-agent-jvm:1.12.2")
testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
testImplementation("com.thedeanda:lorem:2.1")
testImplementation("org.openapi4j:openapi-operation-validator:1.0.6")

View File

@@ -1,48 +0,0 @@
version: '3.8'
services:
sonarqube:
container_name: ${APP_NAME}_sonarqube
image: sonarqube:community
depends_on:
- sonarqube_db
ports:
- ${SONARQUBE_PORT}:9000
networks:
- sonarnet
environment:
SONAR_JDBC_URL: jdbc:postgresql://sonarqube_db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
- sonarqube_temp:/opt/sonarqube/temp
sonarqube_db:
container_name: ${APP_NAME}_sonarqube_db
image: postgres:alpine
networks:
- sonarnet
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
ports:
- ${SONARQUBE_DB_PORT}:5432
volumes:
- sonarqube_postgresql:/var/lib/postgresql
# This needs explicit mapping due to https://github.com/docker-library/postgres/blob/4e48e3228a30763913ece952c611e5e9b95c8759/Dockerfile.template#L52
- sonarqube_postgresql_data:/var/lib/postgresql/data
networks:
sonarnet:
driver: bridge
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
sonarqube_temp:
sonarqube_postgresql:
sonarqube_postgresql_data:

View File

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

View File

@@ -38,6 +38,9 @@ services:
REDIS_CONNECTION: ${REDIS_CONNECTION}
RABBITMQ_CONNECTION: ${RABBITMQ_CONNECTION}
ELASTICSEARCH_CONNECTION: ${ELASTICSEARCH_CONNECTION}
JWT_SECRET: ${JWT_SECRET}
JWT_ISSUER: ${JWT_ISSUER}
JWT_VALIDITY: ${JWT_VALIDITY}
depends_on:
- elasticsearch
- db

275
gradle.lockfile Normal file
View 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

View File

@@ -1,9 +1,7 @@
kotlin.code.style=official
systemProp.sonar.host.url=http://localhost:9002
systemProp.sonar.login=admin
systemProp.sonar.password=sonar
systemProp.sonar.host.url=https://sonarcloud.io
systemProp.sonar.projectKey=dc-project
systemProp.sonar.projectName=DC Project
systemProp.sonar.organization=flecomte
systemProp.sonar.java.coveragePlugin=jacoco
systemProp.sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
systemProp.sonar.kotlin.detekt.reportPaths=build/reports/detekt/detekt.xml

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists

View File

@@ -6,17 +6,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.joda.JodaModule
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
import fr.dcproject.application.Env.PROD
import fr.dcproject.application.Env.TEST
import fr.dcproject.common.security.AccessDeniedException
import fr.dcproject.application.http.statusPagesInstallation
import fr.dcproject.common.utils.onApplicationStopped
import fr.dcproject.component.article.articleKoinModule
import fr.dcproject.component.article.routes.installArticleRoutes
import fr.dcproject.component.auth.ForbiddenException
import fr.dcproject.component.auth.authKoinModule
import fr.dcproject.component.auth.jwt.jwtInstallation
import fr.dcproject.component.auth.routes.installAuthRoutes
import fr.dcproject.component.auth.user
import fr.dcproject.component.citizen.citizenKoinModule
import fr.dcproject.component.citizen.routes.installCitizenRoutes
import fr.dcproject.component.comment.article.routes.installCommentArticleRoutes
@@ -28,8 +26,10 @@ import fr.dcproject.component.constitution.routes.installConstitutionRoutes
import fr.dcproject.component.doc.routes.installDocRoutes
import fr.dcproject.component.follow.followKoinModule
import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes
import fr.dcproject.component.follow.routes.citizen.installFollowCitizenRoutes
import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes
import fr.dcproject.component.notification.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.opinion.opinionKoinModule
import fr.dcproject.component.opinion.routes.installOpinionRoutes
@@ -40,8 +40,6 @@ import fr.dcproject.component.workgroup.routes.installWorkgroupRoutes
import fr.dcproject.component.workgroup.workgroupKoinModule
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.client.HttpClient
@@ -51,20 +49,16 @@ import io.ktor.features.CORS
import io.ktor.features.CallLogging
import io.ktor.features.ContentNegotiation
import io.ktor.features.DataConversion
import io.ktor.features.NotFoundException
import io.ktor.features.StatusPages
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.cio.websocket.pingPeriod
import io.ktor.http.cio.websocket.timeout
import io.ktor.jackson.jackson
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.server.jetty.EngineMain
import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog
@@ -73,14 +67,12 @@ import org.koin.ktor.ext.Koin
import org.koin.ktor.ext.get
import org.slf4j.event.Level
import java.time.Duration
import java.util.concurrent.CompletionException
fun main(args: Array<String>): Unit = EngineMain.main(args)
enum class Env { PROD, TEST }
@ExperimentalCoroutinesApi
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module(env: Env = PROD) {
@@ -125,14 +117,17 @@ fun Application.module(env: Env = PROD) {
masking = false
}
get<NotificationConsumer>().run {
get<NotificationEmailConsumer>().run {
start()
environment.monitor.subscribe(ApplicationStopped) {
close()
}
onApplicationStopped { close() }
}
install(Authentication, jwtInstallation(get()))
get<NotificationPushConsumer>().run {
start()
onApplicationStopped { close() }
}
install(Authentication, jwtInstallation(get(), get()))
install(AutoHeadResponse)
@@ -162,6 +157,7 @@ fun Application.module(env: Env = PROD) {
installCommentRoutes()
installFollowArticleRoutes()
installFollowConstitutionRoutes()
installFollowCitizenRoutes()
installWorkgroupRoutes()
installOpinionRoutes()
installVoteRoutes()
@@ -171,26 +167,7 @@ fun Application.module(env: Env = PROD) {
installDocRoutes()
}
install(StatusPages) {
exception<CompletionException> { e ->
val parent = e.cause?.cause
if (parent is GenericDatabaseException) {
call.respond(HttpStatusCode.BadRequest, parent.errorMessage.message!!)
} else {
throw e
}
}
exception<NotFoundException> { e ->
call.respond(HttpStatusCode.NotFound, e.message!!)
}
exception<AccessDeniedException> {
if (call.user == null) call.respond(HttpStatusCode.Unauthorized)
else call.respond(HttpStatusCode.Forbidden)
}
exception<ForbiddenException> {
call.respond(HttpStatusCode.Forbidden)
}
}
install(StatusPages, statusPagesInstallation())
install(CORS) {
method(HttpMethod.Options)

View File

@@ -43,4 +43,15 @@ class Configuration(val config: Config) {
val rabbitmq: String = config.getString("rabbitmq.connection")
val exchangeNotificationName = "notification"
val sendGridKey: String = config.getString("mail.sendGrid.key")
interface Jwt {
val secret: String
val issuer: String
val validityInMs: Int
}
val jwt = object : Jwt {
override val secret = config.getString("jwt.secret")
override val issuer = config.getString("jwt.issuer")
override val validityInMs = config.getInt("jwt.validity")
}
}

View File

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

View File

@@ -9,22 +9,35 @@ import com.fasterxml.jackson.datatype.joda.JodaModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.common.email.Mailer
import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.NotificationEmailSender
import fr.dcproject.component.notification.NotificationsPush
import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.auth.jwt.JwtConfig
import fr.dcproject.component.notification.NotificationPublisherAsync
import fr.dcproject.component.notification.email.NotificationEmailConsumer
import fr.dcproject.component.notification.email.NotificationEmailSender
import fr.dcproject.component.notification.push.NotificationPushConsumer
import fr.dcproject.component.notification.push.NotificationPushListener
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient
import org.koin.core.qualifier.named
import org.koin.dsl.module
@KtorExperimentalAPI
val KoinModule = module {
// JWT
single {
val config: Configuration = get()
JwtConfig(
config.jwt.secret,
config.jwt.issuer,
config.jwt.validityInMs,
)
}
// JWT Verifier
single {
get<JwtConfig>().verifier
}
// SQL connection
single {
val config: Configuration = get()
@@ -51,11 +64,15 @@ val KoinModule = module {
}
}
single { NotificationsPush.Builder(get()) }
single { NotificationPushListener.Builder(get()) }
single {
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
@@ -100,7 +117,7 @@ val KoinModule = module {
single {
val config: Configuration = get()
Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
NotificationPublisherAsync(factory = get(), exchangeName = config.exchangeNotificationName)
}
single {

View File

@@ -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())
}
}

View File

@@ -0,0 +1,86 @@
package fr.dcproject.application.http
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
import fr.dcproject.common.security.AccessDeniedException
import fr.dcproject.component.auth.ForbiddenException
import fr.dcproject.component.auth.user
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.features.ParameterConversionException
import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import java.util.concurrent.CompletionException
class HttpError(
statusCode: HttpStatusCode,
cause: Throwable? = null,
val title: String = cause?.message ?: statusCode.description,
) {
val statusCode: Int = statusCode.value
}
fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = {
exception<CompletionException> { e ->
val parent = e.cause?.cause
if (parent is GenericDatabaseException) {
HttpError(
HttpStatusCode.BadRequest,
cause = parent
).let {
call.respond(HttpStatusCode.BadRequest, it)
}
} else {
HttpError(
HttpStatusCode.BadRequest,
cause = e
).let {
call.respond(HttpStatusCode.InternalServerError, it)
}
}
}
exception<NotFoundException> { e ->
HttpError(
HttpStatusCode.NotFound,
cause = e
).let {
call.respond(HttpStatusCode.NotFound, it)
}
}
exception<AccessDeniedException> { e ->
if (call.user == null) {
HttpError(
HttpStatusCode.Unauthorized,
cause = e
).let {
call.respond(HttpStatusCode.Unauthorized, it)
}
} else {
HttpError(
HttpStatusCode.Forbidden,
cause = e
).let {
call.respond(HttpStatusCode.Forbidden, it)
}
}
}
exception<ForbiddenException> { e ->
HttpError(
HttpStatusCode.Forbidden,
cause = e
).let {
call.respond(HttpStatusCode.Forbidden, it)
}
}
exception<BadRequestException> { e ->
call.respond(HttpStatusCode.BadRequest, e.httpError)
}
exception<ParameterConversionException> { e ->
val parent = e.cause
if (parent is BadRequestException) {
call.respond(HttpStatusCode.BadRequest, parent.httpError)
} else {
throw e
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
}
}

View File

@@ -0,0 +1,4 @@
package fr.dcproject.common.utils
fun String.isInt(): Boolean = this.toIntOrNull() != null
fun String.isBool(): Boolean = this == "true" || this == "false"

View 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)
}

View 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
}

View 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(""".+@.+\..+""")

View 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
}

View 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
}
}

View 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
}
}

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,13 @@
package fr.dcproject.component.article
package fr.dcproject.component.article.database
import fr.dcproject.common.entity.VersionableId
import fr.dcproject.common.utils.contentToString
import fr.dcproject.common.utils.getJsonField
import fr.dcproject.common.utils.toIso
import fr.dcproject.component.article.database.ArticleI
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.views.ViewManager
import fr.dcproject.component.views.ViewRepository
import fr.dcproject.component.views.entity.ViewAggregation
import org.elasticsearch.client.Request
import org.elasticsearch.client.Response
import org.elasticsearch.client.RestClient
import org.joda.time.DateTime
import java.util.UUID
@@ -17,11 +15,11 @@ import java.util.UUID
/**
* Wrapper for manage views with elasticsearch
*/
class ArticleViewManager <A> (private val restClient: RestClient) : ViewManager<A> where A : VersionableId, A : ArticleI {
class ArticleViewRepository <A> (private val restClient: RestClient) : ViewRepository<A> where A : VersionableId, A : ArticleI {
/**
* Add view on article to elasticsearch
*/
override fun addView(ip: String, entity: A, citizen: CitizenI?, dateTime: DateTime): Response? {
override fun addView(ip: String, entity: A, citizen: CitizenI?, dateTime: DateTime) {
val isLogged = (citizen != null).toString()
val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!!
val request = Request(
@@ -45,7 +43,7 @@ class ArticleViewManager <A> (private val restClient: RestClient) : ViewManager<
)
}
return restClient.performRequest(request)
restClient.performRequest(request)
}
/**
@@ -59,33 +57,33 @@ class ArticleViewManager <A> (private val restClient: RestClient) : ViewManager<
//language=JSON
setJsonEntity(
"""
{
"size": 0,
"query": {
"bool": {
"must": {
"term": {
"version_id": "${entity.versionId}"
{
"size": 0,
"query": {
"bool": {
"must": {
"term": {
"version_id": "${entity.versionId}"
}
}
}
},
"aggs" : {
"total": {
"composite" : {
"sources" : [
{ "version_id": { "terms": {"field": "version_id" } } }
]
}
},
"unique" : {
"cardinality" : {
"field" : "user_ref",
"precision_threshold": 1
}
}
}
}
},
"aggs" : {
"total": {
"composite" : {
"sources" : [
{ "version_id": { "terms": {"field": "version_id" } } }
]
}
},
"unique" : {
"cardinality" : {
"field" : "user_ref",
"precision_threshold": 1
}
}
}
}
""".trimIndent()
)
}

View File

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

View File

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

View File

@@ -2,10 +2,10 @@ package fr.dcproject.component.article.routes
import fr.dcproject.common.security.assert
import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.article.database.ArticleViewRepository
import fr.dcproject.component.auth.citizenOrNull
import io.ktor.application.call
import io.ktor.features.NotFoundException
@@ -24,10 +24,10 @@ object GetOneArticle {
val article = ArticleRef(article)
}
fun Route.getOneArticle(viewManager: ArticleViewManager<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
fun Route.getOneArticle(viewRepository: ArticleViewRepository<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
get<ArticleRequest> {
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
ac.assert { canView(article, citizenOrNull) }
ac.canView(article, citizenOrNull).assert()
call.respond(
article.let { a ->
@@ -64,7 +64,7 @@ object GetOneArticle {
val total: Int = a.votes.total
val score: Int = a.votes.score
}
val views: Any = viewManager.getViewsCount(article).let { v ->
val views: Any = viewRepository.getViewsCount(article).let { v ->
object {
val total = v.total
val unique = v.unique
@@ -76,7 +76,7 @@ object GetOneArticle {
)
launch {
viewManager.addView(call.request.local.remoteHost, article, citizenOrNull)
viewRepository.addView(call.request.local.remoteHost, article, citizenOrNull)
}
}
}

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,16 @@ package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWT
import fr.dcproject.component.auth.database.UserI
import org.koin.core.context.GlobalContext
/**
* Produce a token for this combination of User and Account
*/
fun UserI.makeToken(): String = JWT.create()
.withSubject("Authentication")
.withIssuer(JwtConfig.issuer)
.withClaim("id", id.toString())
.withExpiresAt(JwtConfig.getExpiration())
.sign(JwtConfig.algorithm)
fun UserI.makeToken(): String = GlobalContext.get().get<JwtConfig>().run {
JWT.create()
.withSubject("Authentication")
.withIssuer(issuer)
.withClaim("id", id.toString())
.withExpiresAt(getExpiration())
.sign(algorithm)
}

View File

@@ -5,11 +5,11 @@ import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import java.util.Date
object JwtConfig {
private const val secret = "zAP5MBA4B4Ijz0MZaS48"
const val issuer = "dc-project.fr"
private const val validityInMs = 3_600_000 * 10 // 10 hours
class JwtConfig(
private val secret: String,
val issuer: String,
private val validityInMs: Int,
) {
// TODO change to RSA512
val algorithm: Algorithm = Algorithm.HMAC512(secret)

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWTVerifier
import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserRepository
import io.ktor.application.ApplicationCall
@@ -9,14 +10,14 @@ import io.ktor.http.auth.HttpAuthHeader
import io.ktor.routing.Routing
import java.util.UUID
fun jwtInstallation(userRepo: UserRepository): Authentication.Configuration.() -> Unit = {
fun jwtInstallation(userRepo: UserRepository, verifier: JWTVerifier): Authentication.Configuration.() -> Unit = {
/**
* Setup the JWT authentication to be used in [Routing].
* If the token is valid, the corresponding [User] is fetched from the database.
* The [User] can then be accessed in each [ApplicationCall].
*/
jwt {
verifier(JwtConfig.verifier)
verifier(verifier)
realm = "dc-project.fr"
validate {
it.payload.getClaim("id").asString()?.let { id ->
@@ -27,7 +28,7 @@ fun jwtInstallation(userRepo: UserRepository): Authentication.Configuration.() -
/* Token in URL */
jwt("url") {
verifier(JwtConfig.verifier)
verifier(verifier)
realm = "dc-project.fr"
authHeader { call ->
call.request.queryParameters["token"]?.let {

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,20 @@
package fr.dcproject.component.citizen.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.CitizenAccessControl
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
@@ -26,12 +32,30 @@ object FindCitizens {
val sort: String? = null,
val direction: RepositoryI.Direction? = 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) {
get<CitizensRequest> {
mustBeAuth()
it.validate().badRequestIfNotValid()
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(
citizens.toOutput { c: CitizenCreator ->
object {

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes
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.CitizenAccessControl
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
@@ -22,11 +23,12 @@ object GetCurrentCitizen {
fun Route.getCurrentCitizen(ac: CitizenAccessControl) {
get<CurrentCitizenRequest> {
mustBeAuth()
val currentUser = citizenOrNull
if (currentUser === null) {
call.respond(HttpStatusCode.Unauthorized)
} else {
ac.assert { canView(currentUser, citizenOrNull) }
ac.canView(currentUser, citizenOrNull).assert()
call.respond(
object {
val id: UUID = citizen.id
@@ -34,7 +36,7 @@ object GetCurrentCitizen {
object {
val firstName: String = n.firstName
val lastName: String = n.lastName
}
}
}
val email: String = citizen.email
val birthday: String = citizen.birthday.run {

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes
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.CitizenAccessControl
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.citizen.database.CitizenRepository
@@ -26,8 +27,9 @@ object GetOneCitizen {
fun Route.getOneCitizen(ac: CitizenAccessControl, citizenRepository: CitizenRepository) {
get<CitizenRequest> {
mustBeAuth()
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(
object {

View File

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

View File

@@ -1,16 +1,20 @@
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.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.article.database.CommentArticleRepository
import fr.dcproject.component.comment.article.routes.CreateCommentArticle.PostArticleCommentRequest.Input
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.toOutput
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -25,26 +29,36 @@ object CreateCommentArticle {
@Location("/articles/{article}/comments")
class PostArticleCommentRequest(article: UUID) {
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) {
post<PostArticleCommentRequest> {
call.receiveOrBadRequest<Input>().run {
CommentForUpdate(
target = it.article,
createdBy = citizen,
content = content
)
}.let { comment ->
ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
mustBeAuth()
call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
.run {
CommentForUpdate(
target = it.article,
createdBy = citizen,
content = content
)
}.let { comment ->
ac.canCreate(comment, citizenOrNull).assert()
repo.comment(comment)
call.respond(
HttpStatusCode.Created,
comment.toOutput()
)
}
call.respond(
HttpStatusCode.Created,
comment.toOutput()
)
}
}
}
}

View File

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

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.comment.article.routes
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.comment.article.database.CommentArticleRepository
import fr.dcproject.component.comment.generic.CommentAccessControl
@@ -25,8 +26,9 @@ object GetCitizenArticleComments {
fun Route.getCitizenArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) {
get<CitizenCommentArticleRequest> {
mustBeAuth()
repo.findByCitizen(it.citizen).let { comments ->
ac.assert { canView(comments.result, citizenOrNull) }
ac.canView(comments.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
comments.toOutput { comment ->

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.comment.constitution.routes
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.comment.constitution.database.CommentConstitutionRepository
import fr.dcproject.component.comment.generic.CommentAccessControl
@@ -25,8 +26,9 @@ object GetCitizenCommentConstitution {
fun Route.getCitizenCommentConstitution(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
get<GetCitizenCommentConstitutionRequest> {
mustBeAuth()
val comments = repo.findByCitizen(it.citizen)
ac.assert { canView(comments.result, citizenOrNull) }
ac.canView(comments.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
comments.toOutput { comment ->

View File

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

View File

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

View File

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

View File

@@ -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())
}
}
}
}

View File

@@ -1,45 +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.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> {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = CommentForUpdate(
content = call.receiveOrBadRequest<CreateCommentChildrenRequest.Input>().content,
createdBy = citizen,
parent = parent
)
ac.assert { canCreate(newComment, citizenOrNull) }
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment.toOutput())
}
}
}

View File

@@ -1,13 +1,18 @@
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.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
@@ -23,21 +28,40 @@ object EditComment {
@Location("/comments/{comment}")
class EditCommentRequest(comment: UUID) {
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) {
put<EditCommentRequest> {
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.assert { canUpdate(comment, citizenOrNull) }
mustBeAuth()
val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.canUpdate(commentOld, citizenOrNull).assert()
comment.content = call.receiveOrBadRequest<EditCommentRequest.Input>().content
repo.edit(comment)
call.respond(
HttpStatusCode.OK,
comment.toOutput()
)
call.receiveOrBadRequest<EditCommentRequest.Input>()
.apply { validate().badRequestIfNotValid() }
.run {
CommentForUpdate(
id = commentOld.id,
createdBy = commentOld.createdBy,
target = commentOld.target,
parent = commentOld.parent,
content = content,
)
}
.let { repo.edit(it) }
.let {
call.respond(
HttpStatusCode.OK,
it.toOutput()
)
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
package fr.dcproject.component.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenWithUserI
import fr.dcproject.component.constitution.ConstitutionAccessControl
@@ -14,6 +16,9 @@ import fr.dcproject.component.constitution.database.ConstitutionForUpdate.TitleF
import fr.dcproject.component.constitution.database.ConstitutionRepository
import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input
import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input.Title
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -35,7 +40,6 @@ object CreateConstitution {
val draft: Boolean = false,
val versionId: UUID = UUID.randomUUID()
) {
class Title(
val id: UUID = UUID.randomUUID(),
val name: String,
@@ -43,10 +47,25 @@ object CreateConstitution {
) {
class ArticleRef(val id: UUID)
}
fun validate() = Validation<Input> {
Input::title {
minLength(10)
maxLength(80)
}
Input::titles onEach {
Title::name {
minLength(10)
maxLength(80)
}
}
}.validate(this)
}
}
private fun getNewConstitution(input: Input, citizen: Citizen) = input.run {
validate().badRequestIfNotValid()
ConstitutionForUpdate<CitizenWithUserI, TitleForUpdate<ArticleRef>>(
id = UUID.randomUUID(),
title = title,
@@ -68,8 +87,9 @@ object CreateConstitution {
fun Route.createConstitution(repo: ConstitutionRepository, ac: ConstitutionAccessControl) {
post<PostConstitutionRequest> {
mustBeAuth()
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")
call.respond(
HttpStatusCode.Created,

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
@@ -8,6 +9,10 @@ import fr.dcproject.component.constitution.database.ConstitutionRepository
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -27,12 +32,29 @@ object FindConstitutions {
val sort: String? = null,
val direction: RepositoryI.Direction? = 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) {
get<FindConstitutionsRequest> {
it.validate().badRequestIfNotValid()
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(
HttpStatusCode.OK,
constitutions.toOutput { c ->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowForUpdate
@@ -25,8 +26,9 @@ object FollowArticle {
fun Route.followArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
post<ArticleFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.assert { canCreate(follow, citizenOrNull) }
ac.canCreate(follow, citizenOrNull).assert()
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}

View File

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

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.article
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.FollowArticleRepository
@@ -25,8 +26,9 @@ object GetMyFollowsArticle {
fun Route.getMyFollowsArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
get<CitizenFollowArticleRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.assert { canView(follows.result, citizenOrNull) }
ac.canView(follows.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowForUpdate
@@ -25,8 +26,9 @@ object UnfollowArticle {
fun Route.unfollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
delete<ArticleFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.assert { canDelete(follow, citizenOrNull) }
ac.canDelete(follow, citizenOrNull).assert()
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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())
}
}

View File

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

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
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.constitution.database.ConstitutionRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository
@@ -25,8 +26,9 @@ object FollowConstitution {
fun Route.followConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
post<ConstitutionFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.assert { canCreate(follow, citizenOrNull) }
ac.canCreate(follow, citizenOrNull).assert()
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}

View File

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

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
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.FollowConstitutionRepository
@@ -25,8 +26,9 @@ object GetMyFollowsConstitution {
fun Route.getMyFollowsConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
get<CitizenFollowConstitutionRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.assert { canView(follows.result, citizenOrNull) }
ac.canView(follows.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
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.constitution.database.ConstitutionRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository
@@ -25,8 +26,9 @@ object UnfollowConstitution {
fun Route.unfollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
delete<ConstitutionUnfollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.assert { canDelete(follow, citizenOrNull) }
ac.canDelete(follow, citizenOrNull).assert()
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}

View File

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

View File

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

View File

@@ -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")
}

View File

@@ -1,5 +1,7 @@
package fr.dcproject.component.notification
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature
@@ -12,7 +14,11 @@ import fr.dcproject.component.article.database.ArticleForView
import org.joda.time.DateTime
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 createdAt: DateTime = DateTime.now()
) {
@@ -42,16 +48,16 @@ open class Notification(
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(
val target: Entity,
open class EntityNotificationMessage <E : Entity> (
open val target: E,
type: String,
val action: String
) : Notification(type)
) : NotificationMessage(type)
class ArticleUpdateNotification(
target: ArticleForView
) : EntityNotification(target, "article", "update")
data class ArticleUpdateNotificationMessage(
override val target: ArticleForView
) : EntityNotificationMessage<ArticleForView>(target, "article", "update")

View File

@@ -7,12 +7,15 @@ import kotlinx.coroutines.coroutineScope
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class Publisher(
class NotificationPublisherAsync(
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,
) {
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 {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->

View File

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

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