1 Commits

Author SHA1 Message Date
a66ef0128b change postgresjson to postgresjson-jdbc 2019-08-22 09:55:12 +02:00
270 changed files with 1175 additions and 13088 deletions

13
.env
View File

@@ -2,13 +2,8 @@ NAME=dc-project
DATABASE_URL=jdbc:postgresql:dc-project DATABASE_URL=jdbc:postgresql:dc-project
APP_PORT=8080
OPENAPI_PORT=8181
SONARQUBE_PORT=9002
ELASTIC_REST=9200 ELASTIC_REST=9200
ELASTIC_NODES=9300 ELASTIC_NODES=9300
ELASTICSEARCH_CONNECTION=http://elasticsearch:9200
POSTGRESQL_PORT=5432 POSTGRESQL_PORT=5432
DB_HOST=db DB_HOST=db
@@ -16,11 +11,3 @@ DB_PORT=5432
DB_NAME=dc-project DB_NAME=dc-project
DB_USER=dc-project DB_USER=dc-project
DB_PWD=dc-project DB_PWD=dc-project
REDIS_PORT=6379
REDIS_CONNECTION=redis://redis:6379
REDIS_COMMANDER_PORT=8081
RABBITMQ_PORT=5672
RABBITMQ_CONNECTION=amqp://rabbitmq:5672
RABBITMQ_MANAGEMENT_PORT=15672

3
.gitignore vendored
View File

@@ -1,6 +1,5 @@
/.gradle /.gradle
/.idea/* /.idea
!/.idea/runConfigurations
/out /out
/build /build
*.ipr *.ipr

1
.idea/.name generated
View File

@@ -1 +0,0 @@
dcproject

View File

@@ -1,6 +1,5 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>

6
.idea/dataSources.xml generated
View File

@@ -7,11 +7,5 @@
<jdbc-driver>org.postgresql.Driver</jdbc-driver> <jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/dc-project</jdbc-url> <jdbc-url>jdbc:postgresql://localhost:5432/dc-project</jdbc-url>
</data-source> </data-source>
<data-source source="LOCAL" name="test@localhost" uuid="a9a6d0e9-327d-4e7d-9b93-3cb6f7948866">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/test</jdbc-url>
</data-source>
</component> </component>
</project> </project>

3
.idea/gradle.xml generated
View File

@@ -4,10 +4,11 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="delegatedBuild" value="false" /> <option name="delegatedBuild" value="true" />
<option name="testRunner" value="PLATFORM" /> <option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="11" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />

8
.idea/misc.xml generated
View File

@@ -1,13 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="corretto-11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

22
.idea/runConfigurations/All_Tests.xml generated Normal file
View File

@@ -0,0 +1,22 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<log_file alias="test.log" path="$PROJECT_DIR$/var/log/test" />
<module name="dcproject.test" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="ALTERNATIVE_JRE_PATH" value="11" />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="directory" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<dir value="$PROJECT_DIR$" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests + Lint" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<method v="2">
<option name="Make" enabled="true" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Lint" run_configuration_type="GradleRunConfiguration" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Test All SQL" run_configuration_type="ShConfigurationType" />
</method>
</configuration>
</component>

View File

@@ -1,25 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests + Lint (offline)" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="tags" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.options=&quot;--tags ~@online&quot; -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Lint" run_configuration_type="GradleRunConfiguration" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Test All SQL" run_configuration_type="ShConfigurationType" />
</method>
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Article Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@article&quot; -Dstrict" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Auth Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@auth&quot;" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,31 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build" 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="build" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" />
</ENTRIES>
</extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build and start all Docker" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envVars">
<list>
<DockerEnvVarImpl>
<option name="name" value="SEND_GRID_KEY" />
<option name="value" value="$SEND_GRID_KEY$" />
</DockerEnvVarImpl>
</list>
</option>
<option name="commandLineOptions" value="--build" />
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2">
<option name="Gradle.BeforeRunTask" enabled="true" tasks="build" externalProjectPath="$PROJECT_DIR$" vmOptions="" scriptParameters="" />
</method>
</configuration>
</component>

View File

@@ -1,31 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build without test" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<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="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="build" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" />
</ENTRIES>
</extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Citizen Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@citizen&quot;" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Comment Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@comment&quot;" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Constitution Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@constitution&quot;" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,30 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Cucumber Tests (offline)" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="fr.dcproject.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot; not @online&quot; -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,13 +1,8 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start Elasticsearch" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker"> <configuration default="false" name="Docker" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml"> <deployment type="docker-compose.yml">
<settings> <settings>
<option name="commandLineOptions" value="--build" /> <option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="elasticsearch" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" /> <option name="sourceFilePath" value="docker-compose.yml" />
</settings> </settings>
</deployment> </deployment>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Follow Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@follow&quot;" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,31 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Lint" 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="ktlintCheck" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" />
</ENTRIES>
</extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,39 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Lint+Test+Sonar &amp; Run" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</map>
</option>
<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="run" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" />
</ENTRIES>
</extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All Tests + Lint" run_configuration_type="JUnit" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Sonarqube" run_configuration_type="GradleRunConfiguration" />
</method>
</configuration>
</component>

View File

@@ -1,38 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Lint, Test &amp; Run" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</map>
</option>
<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="run" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" />
</ENTRIES>
</extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All Tests + Lint" run_configuration_type="JUnit" />
</method>
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Mark as @error" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@error&quot; -Dstrict" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Opinion Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@opinion&quot;" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,13 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Reset DB on DEV" type="ShConfigurationType">
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/src/main/resources/sql/resetDB.sh" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/src/main/resources/sql" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="false" />
<option name="INTERPRETER_PATH" value="C:/Program Files/Git/bin/bash.exe" />
<option name="INTERPRETER_OPTIONS" value="" />
<method v="2" />
</configuration>
</component>

View File

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

View File

@@ -1,17 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Cucumber Tests" type="JUnit" factoryName="JUnit"> <configuration default="false" name="RunCucumberTest" type="JUnit" factoryName="JUnit" nameIsGenerated="true">
<output_file path="$PROJECT_DIR$/var/log/test/cucumber.out.log" /> <output_file path="$PROJECT_DIR$/var/log/test/cucumber.out.log" is_save="true" />
<log_file alias="cucumber.log" path="$PROJECT_DIR$/var/log/test" />
<module name="dcproject.test" /> <module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" /> <option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" /> <option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="testArticle" />
<option name="TEST_OBJECT" value="class" /> <option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

View File

@@ -1,27 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run dependencies" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envVars">
<list>
<DockerEnvVarImpl>
<option name="name" value="SEND_GRID_KEY" />
<option name="value" value="$SEND_GRID_KEY$" />
</DockerEnvVarImpl>
</list>
</option>
<option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="db" />
<option value="elasticsearch" />
<option value="rabbitmq" />
<option value="redis" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@@ -1,13 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="SQL Fixtures on DEV" type="ShConfigurationType">
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/src/main/resources/sql/fixtures/fixtures.sh" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/src/main/resources/sql/fixtures/" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="false" />
<option name="INTERPRETER_PATH" value="C:/Program Files/Git/bin/bash.exe" />
<option name="INTERPRETER_OPTIONS" value="" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-x test" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="sonarqube" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,26 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start App" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envVars">
<list>
<DockerEnvVarImpl>
<option name="name" value="SEND_GRID_KEY" />
<option name="value" value="$SEND_GRID_KEY$" />
</DockerEnvVarImpl>
</list>
</option>
<option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="app" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Build without test" run_configuration_type="GradleRunConfiguration" />
</method>
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start Db" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envVars">
<list>
<DockerEnvVarImpl>
<option name="name" value="SEND_GRID_KEY" />
<option name="value" value="$SEND_GRID_KEY$" />
</DockerEnvVarImpl>
</list>
</option>
<option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="db" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start OpenAPI" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envVars">
<list>
<DockerEnvVarImpl>
<option name="name" value="SEND_GRID_KEY" />
<option name="value" value="$SEND_GRID_KEY$" />
</DockerEnvVarImpl>
</list>
</option>
<option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="openapi" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start RabbitMQ" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envVars">
<list>
<DockerEnvVarImpl>
<option name="name" value="SEND_GRID_KEY" />
<option name="value" value="$SEND_GRID_KEY$" />
</DockerEnvVarImpl>
</list>
</option>
<option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="rabbitmq" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start Redis" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envVars">
<list>
<DockerEnvVarImpl>
<option name="name" value="SEND_GRID_KEY" />
<option name="value" value="$SEND_GRID_KEY$" />
</DockerEnvVarImpl>
</list>
</option>
<option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="redis" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@@ -1,13 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test All SQL" type="ShConfigurationType">
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/src/test/sql/test.sh" />
<option name="SCRIPT_OPTIONS" value="1" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/src/test/sql" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="false" />
<option name="INTERPRETER_PATH" value="C:/Program Files/Git/bin/bash.exe" />
<option name="INTERPRETER_OPTIONS" value="" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Vote Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@vote&quot;" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,29 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Voter Tests" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="fr.dcproject.security.voter.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="tags" />
<option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="voter" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,30 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Workgroup test" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<extension name="coverage" sample_coverage="false">
<pattern>
<option name="PATTERN" value="fr.dcproject.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@workgroup&quot;" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,113 +1,49 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.owasp.dependencycheck.reporting.ReportGenerator
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
val ktor_version: String by project val ktor_version: String by project
val kotlin_version: String by project val kotlin_version: String by project
val coroutinesVersion: String by project
val logback_version: String by project val logback_version: String by project
val koinVersion: String by project val koinVersion: String by project
val jackson_version: String by project val postgresjson_version: String by project
val cucumber_version: String by project
group = "com.github.flecomte"
version = "0.0.1"
plugins { plugins {
jacoco
application application
kotlin("jvm") version "1.3.40"
id("maven-publish")
id("org.jetbrains.kotlin.jvm") version "1.3.50"
id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jlleitschuh.gradle.ktlint") version "8.2.0"
id("org.owasp.dependencycheck") version "5.1.0"
id("org.sonarqube") version "2.7"
} }
group = "fr.dcproject"
version = "0.0.1"
application { application {
mainClassName = "io.ktor.server.jetty.EngineMain" mainClassName = "io.ktor.server.netty.EngineMain"
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
}
}
tasks.withType<Jar> {
manifest {
attributes(
mapOf(
"Main-Class" to application.mainClassName
)
)
}
}
tasks {
named<ShadowJar>("shadowJar") {
mergeServiceFiles("META-INF/services")
}
}
jacoco {
toolVersion = "0.8.3"
}
tasks.jacocoTestReport {
reports {
xml.isEnabled = true
}
}
dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
} }
repositories { repositories {
mavenLocal() mavenLocal()
jcenter() jcenter()
maven { url = uri("https://kotlin.bintray.com/ktor") } maven { url = uri("https://kotlin.bintray.com/ktor") }
maven { url = uri("https://jitpack.io") }
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutinesVersion")
implementation("io.ktor:ktor-server-jetty:$ktor_version")
implementation("io.ktor:ktor-client-jetty:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version") implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-core:$ktor_version") implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-locations:$ktor_version") implementation("io.ktor:ktor-locations:$ktor_version")
implementation("io.ktor:ktor-auth:$ktor_version") implementation("io.ktor:ktor-auth:$ktor_version")
implementation("io.ktor:ktor-auth-jwt:$ktor_version") implementation("io.ktor:ktor-auth-jwt:$ktor_version")
implementation("io.ktor:ktor-gson:$ktor_version") implementation("io.ktor:ktor-gson:$ktor_version")
implementation("io.ktor:ktor-auth-jwt:$ktor_version")
implementation("io.ktor:ktor-websockets:$ktor_version")
implementation("org.koin:koin-ktor:$koinVersion") implementation("org.koin:koin-ktor:$koinVersion")
implementation("io.ktor:ktor-jackson:$ktor_version") implementation("io.ktor:ktor-jackson:$ktor_version")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.9")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jackson_version") implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.9.9")
implementation("net.pearx.kasechange:kasechange-jvm:1.1.0") implementation("net.pearx.kasechange:kasechange-jvm:1.1.0")
implementation("com.auth0:java-jwt:3.8.2") implementation("fr.postgresjson:postgresjson-jdbc:$postgresjson_version")
implementation("com.github.jasync-sql:jasync-postgresql:1.0.7")
implementation("com.github.flecomte:postgres-json:1.1.1")
implementation("com.github.flecomte:ktor-voter:1.0.1")
implementation("com.sendgrid:sendgrid-java:4.4.1")
implementation("io.lettuce:lettuce-core:5.2.2.RELEASE")
implementation("com.rabbitmq:amqp-client:5.8.0")
implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
implementation("com.jayway.jsonpath:json-path:2.4.0")
testImplementation("io.ktor:ktor-server-tests:$ktor_version") testImplementation("io.ktor:ktor-server-tests:$ktor_version")
testImplementation("io.ktor:ktor-client-mock:$ktor_version") testImplementation("io.ktor:ktor-client-mock:$ktor_version")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktor_version") testImplementation("io.ktor:ktor-client-mock-jvm:$ktor_version")
testImplementation("org.koin:koin-test:$koinVersion") testImplementation("org.koin:koin-test:$koinVersion")
testImplementation("io.mockk:mockk:1.9.3") testImplementation("io.mockk:mockk:1.9")
testImplementation("org.junit.jupiter:junit-jupiter:5.5.0") testImplementation("org.junit.jupiter:junit-jupiter:5.5.0")
testImplementation("org.amshove.kluent:kluent:1.4") testImplementation("org.amshove.kluent:kluent:1.4")
testImplementation("io.cucumber:cucumber-java8:$cucumber_version") testImplementation("io.cucumber:cucumber-java8:4.3.1")
testImplementation("io.cucumber:cucumber-junit:$cucumber_version") testImplementation("io.cucumber:cucumber-junit:4.3.1")
} }

View File

@@ -2,61 +2,10 @@
# Add the "-d" flag at the end for detached execution # Add the "-d" flag at the end for detached execution
version: '3.7' version: '3.7'
services: services:
sonarqube:
container_name: sonarqube_${NAME}
image: sonarqube
restart: always
ports:
- ${SONARQUBE_PORT}:9000
openapi:
container_name: openapi_${NAME}
image: swaggerapi/swagger-ui
restart: always
ports:
- ${OPENAPI_PORT}:8080
environment:
URL: "http://localhost:8080"
rabbitmq:
container_name: rabbitmq_${NAME}
image: rabbitmq:management-alpine
restart: always
ports:
- ${RABBITMQ_PORT}:5672
- ${RABBITMQ_MANAGEMENT_PORT}:15672
redis:
container_name: redis_${NAME}
image: redis:6.0-rc-alpine
restart: always
ports:
- ${REDIS_PORT}:6379
app:
container_name: app_${NAME}
build:
context: ./build
dockerfile: ../docker/app/Dockerfile
restart: always
ports:
- ${APP_PORT}:8080
environment:
DB_HOST: ${DB_HOST}
SEND_GRID_KEY: ${SEND_GRID_KEY}
REDIS_CONNECTION: ${REDIS_CONNECTION}
RABBITMQ_CONNECTION: ${RABBITMQ_CONNECTION}
ELASTICSEARCH_CONNECTION: ${ELASTICSEARCH_CONNECTION}
depends_on:
- elasticsearch
- db
- redis
- rabbitmq
elasticsearch: elasticsearch:
container_name: elasticsearch_${NAME} container_name: elasticsearch_${NAME}
image: elasticsearch:6.7.1 image: elasticsearch:6.7.1
restart: always
ports: ports:
- ${ELASTIC_REST}:9200 - ${ELASTIC_REST}:9200
- ${ELASTIC_NODES}:9300 - ${ELASTIC_NODES}:9300

View File

@@ -1,14 +0,0 @@
FROM adoptopenjdk/openjdk11:jre-11.0.4_11-alpine
ENV APPLICATION_USER ktor
RUN adduser -D -g '' $APPLICATION_USER
RUN mkdir /app
RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER
COPY ./libs/dcproject-0.0.1-all.jar /app/dcproject.jar
WORKDIR /app
CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:InitialRAMFraction=2", "-XX:MinRAMFraction=2", "-XX:MaxRAMFraction=2", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "dcproject.jar"]

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/bash
set -e set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL

View File

@@ -1,10 +1,6 @@
ktor_version=1.2.2 ktor_version=1.2.2
kotlin.code.style=official kotlin.code.style=official
kotlin_version=1.3.40 kotlin_version=1.3.40
coroutinesVersion=1.3.3
logback_version=1.2.1 logback_version=1.2.1
postgresjson_version=0.1
koinVersion=2.0.1 koinVersion=2.0.1
jackson_version=2.9.9
cucumber_version=5.1.3
systemProp.sonar.host.url=http://localhost:9000
systemProp.sonar.login=1196e8015c20035f1aa91e881b95ce9d6e879c8a

Binary file not shown.

View File

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

22
gradlew vendored
View File

@@ -1,21 +1,5 @@
#!/usr/bin/env sh #!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
############################################################################## ##############################################################################
## ##
## Gradle start up script for UN*X ## Gradle start up script for UN*X
@@ -44,7 +28,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"
@@ -125,8 +109,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`

18
gradlew.bat vendored
View File

@@ -1,19 +1,3 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@@ -30,7 +14,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS=
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome

View File

@@ -1,364 +0,0 @@
package fr.dcproject
import com.fasterxml.jackson.core.util.DefaultIndenter
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategy
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.Env.PROD
import fr.dcproject.entity.*
import fr.dcproject.event.EventNotification
import fr.dcproject.event.EventSubscriber
import fr.dcproject.routes.*
import fr.dcproject.security.voter.*
import fr.ktorVoter.AuthorizationVoter
import fr.ktorVoter.ForbiddenException
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.auth.authenticate
import io.ktor.auth.jwt.jwt
import io.ktor.client.HttpClient
import io.ktor.client.engine.jetty.Jetty
import io.ktor.features.*
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.auth.HttpAuthHeader
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.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets
import org.eclipse.jetty.util.log.Slf4jLog
import org.elasticsearch.client.Request
import org.elasticsearch.client.RestClient
import org.koin.core.qualifier.named
import org.koin.ktor.ext.Koin
import org.koin.ktor.ext.get
import org.slf4j.event.Level
import java.time.Duration
import java.util.*
import java.util.concurrent.CompletionException
import fr.dcproject.entity.Workgroup as WorkgroupEntity
import fr.dcproject.repository.Article as RepositoryArticle
import fr.dcproject.repository.Citizen as RepositoryCitizen
import fr.dcproject.repository.Constitution as RepositoryConstitution
import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository
import fr.dcproject.repository.User as UserRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepository
fun main(args: Array<String>): Unit = io.ktor.server.jetty.EngineMain.main(args)
enum class Env { PROD, TEST, CUCUMBER }
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module(env: Env = PROD) {
install(Koin) {
Slf4jLog()
modules(Module)
}
install(CallLogging) {
level = Level.INFO
}
install(DataConversion) {
convert<UUID> {
decode { values, _ ->
values.singleOrNull()?.let { UUID.fromString(it) }
}
encode { value ->
when (value) {
null -> listOf()
is UUID -> listOf(value.toString())
else -> throw InternalError("Cannot convert $value as UUID")
}
}
}
// TODO: create generic convert for entityI
convert<Article> {
decode { values, _ ->
values.singleOrNull()?.let {
get<RepositoryArticle>().findById(UUID.fromString(it))
?: throw NotFoundException("Article $values not found")
} ?: throw NotFoundException("Article $values not found")
}
}
convert<ArticleRef> {
decode { values, _ ->
values.singleOrNull()?.let {
ArticleRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Article""")
}
}
convert<CommentRef> {
decode { values, _ ->
values.singleOrNull()?.let {
CommentRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Comment""")
}
}
convert<ConstitutionRef> {
decode { values, _ ->
values.singleOrNull()?.let {
ConstitutionRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Constitution""")
}
}
convert<Constitution> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<RepositoryConstitution>().findById(id) ?: throw NotFoundException("Constitution $values not found")
}
}
convert<Citizen> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<RepositoryCitizen>().findById(id, true) ?: throw NotFoundException("Citizen $values not found")
}
}
convert<CitizenRef> {
decode { values, _ ->
values.singleOrNull()?.let {
CitizenRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Citizen""")
}
}
convert<OpinionChoice> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<OpinionChoiceRepository>().findOpinionChoiceById(id)
?: throw NotFoundException("OpinionChoice $values not found")
}
}
convert<WorkgroupRef> {
decode { values, _ ->
values.singleOrNull()?.let {
WorkgroupRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Workgroup""")
}
}
convert<WorkgroupEntity> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<WorkgroupRepository>().findById(id)
?: throw NotFoundException("Workgroup $values not found")
}
}
}
install(Locations) {
}
install(AuthorizationVoter) {
voters = mutableListOf(
ArticleVoter(),
ConstitutionVoter(),
CitizenVoter(),
CommentVoter(),
VoteVoter(),
FollowVoter(),
OpinionVoter(),
OpinionChoiceVoter(),
WorkgroupVoter()
)
}
HttpClient(Jetty) {
engine {
}
}
/* Create index if not exist */
get<RestClient>().run {
if (performRequest(Request("HEAD", "/views?include_type_name=false")).statusLine.statusCode == 404) {
Request(
"PUT",
"/views?include_type_name=false"
).apply {
//language=JSON
setJsonEntity(
"""
{
"settings": {
"number_of_shards": 5
},
"mappings": {
"properties": {
"logged": {
"type": "boolean"
},
"type": {
"type": "keyword"
},
"user_ref": {
"type": "keyword"
},
"id": {
"type": "keyword"
},
"version_id": {
"type": "keyword"
},
"ip": {
"type": "keyword"
},
"citizen_id": {
"type": "keyword"
},
"view_at": {
"type": "date"
}
}
}
}
""".trimIndent()
)
}.let {
performRequest(it)
}
}
}
install(WebSockets) {
pingPeriod = Duration.ofSeconds(60) // Disabled (null) by default
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE // Disabled (max value). The connection will be closed if surpassed this length.
masking = false
}
install(EventSubscriber) {
EventNotification(this, get(), get(), get(), get(), get()).config()
}
install(Authentication) {
/**
* 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)
realm = "dc-project.fr"
validate {
it.payload.getClaim("id").asString()?.let { id ->
get<UserRepository>().findById(UUID.fromString(id))
}
}
}
jwt("url") {
verifier(JwtConfig.verifier)
realm = "dc-project.fr"
authHeader { call ->
call.request.queryParameters.get("token")?.let {
HttpAuthHeader.Single("Bearer", it)
}
}
validate {
it.payload.getClaim("id").asString()?.let { id ->
get<UserRepository>().findById(UUID.fromString(id))
}
}
}
}
install(AutoHeadResponse)
install(ContentNegotiation) {
jackson {
propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
registerModule(JodaModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
configure(SerializationFeature.INDENT_OUTPUT, true)
setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
indentObjectsWith(DefaultIndenter(" ", "\n"))
})
}
}
install(Routing) {
// trace { application.log.trace(it.buildText()) }
authenticate(optional = true) {
article(get(), get())
auth(get(), get(), get())
citizen(get(), get())
constitution(get())
followArticle(get())
followConstitution(get())
comment(get())
commentArticle(get())
commentConstitution(get())
voteArticle(get(), get(), get())
voteConstitution(get())
opinionArticle(get())
opinionChoice(get())
workgroup(get())
definition()
}
authenticate("url") {
notificationArticle(get(), get(named("ws")))
}
}
install(StatusPages) {
// TODO move to postgresJson lib
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<ForbiddenException> {
call.respond(HttpStatusCode.Forbidden)
}
}
install(CORS) {
method(HttpMethod.Options)
method(HttpMethod.Put)
method(HttpMethod.Delete)
header(HttpHeaders.Authorization)
anyHost()
// host("localhost:4200", schemes = listOf("http", "https"))
allowCredentials = true
allowSameOrigin = true
maxAge = Duration.ofDays(1)
}
if (env == PROD) {
get<Migrations>().run()
}
}

View File

@@ -1,31 +0,0 @@
package fr.dcproject
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.ktorVoter.ForbiddenException
import io.ktor.application.ApplicationCall
import io.ktor.auth.authentication
import io.ktor.util.AttributeKey
import io.ktor.util.pipeline.PipelineContext
import org.koin.core.context.GlobalContext
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.Citizen as CitizenRepository
private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
val ApplicationCall.citizen: CitizenEntity
get() = attributes.computeIfAbsent(citizenAttributeKey) {
val user = authentication.principal<UserI>() ?: throw ForbiddenException()
GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"")
}
val ApplicationCall.citizenOrNull: CitizenEntity?
get() = authentication.principal<UserI>()?.let {
GlobalContext.get().koin.get<CitizenRepository>().findByUser(it)
}
val PipelineContext<Unit, ApplicationCall>.citizen get() = context.citizen
val PipelineContext<Unit, ApplicationCall>.citizenOrNull get() = context.citizenOrNull
val ApplicationCall.user get() = authentication.principal<User>()

View File

@@ -1,62 +0,0 @@
package fr.dcproject
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.typesafe.config.ConfigFactory
import fr.dcproject.entity.UserI
import java.util.*
import java.net.URI
object Config {
private var config = ConfigFactory.load()
object Sql {
val migrationFiles: URI = this::class.java.getResource("/sql/migrations").toURI()
val functionFiles: URI = this::class.java.getResource("/sql/functions").toURI()
val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures").toURI()
}
val envName: String = config.getString("app.envName")
val domain: String = config.getString("app.domain")
val host: String = config.getString("db.host")
var database: String = config.getString("db.database")
var username: String = config.getString("db.username")
var password: String = config.getString("db.password")
val port: Int = config.getInt("db.port")
val redis: String = config.getString("redis.connection")
val elasticsearch: String = config.getString("elasticsearch.connection")
val rabbitmq: String = config.getString("rabbitmq.connection")
val exchangeNotificationName = "notification"
val sendGridKey: String = config.getString("mail.sendGrid.key")
}
object JwtConfig {
private const val secret = "zAP5MBA4B4Ijz0MZaS48"
const val issuer = "dc-project.fr"
private const val validityInMs = 3_600_000 * 10 // 10 hours
// TODO change to RSA512
val algorithm = Algorithm.HMAC512(secret)
val verifier: JWTVerifier = JWT
.require(algorithm)
.withIssuer(issuer)
.build()
/**
* Produce a token for this combination of User and Account
*/
fun makeToken(user: UserI): String = JWT.create()
.withSubject("Authentication")
.withIssuer(issuer)
.withClaim("id", user.id.toString())
.withExpiresAt(getExpiration())
.sign(algorithm)
/**
* Calculate the expiration Date based on current time + the given validity
*/
private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
}

View File

@@ -1,135 +0,0 @@
package fr.dcproject
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.joda.JodaModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.event.publisher.Publisher
import fr.dcproject.messages.Mailer
import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.messages.SsoManager
import fr.dcproject.views.ArticleViewManager
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 io.lettuce.core.api.async.RedisAsyncCommands
import org.apache.http.HttpHost
import org.elasticsearch.client.RestClient
import org.koin.core.qualifier.named
import org.koin.dsl.module
import fr.dcproject.repository.Article as ArticleRepository
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.CommentArticle as CommentArticleRepository
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository
import fr.dcproject.repository.CommentGeneric as CommentGenericRepository
import fr.dcproject.repository.Constitution as ConstitutionRepository
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository
import fr.dcproject.repository.User as UserRepository
import fr.dcproject.repository.VoteArticle as VoteArticleRepository
import fr.dcproject.repository.VoteComment as VoteCommentRepository
import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepository
@KtorExperimentalAPI
val Module = module {
single { Config }
// SQL connection
single {
Connection(
host = Config.host,
port = Config.port,
database = Config.database,
username = Config.username,
password = Config.password
)
}
// Launch Database migration
single { Migrations(get(), Config.Sql.migrationFiles, Config.Sql.functionFiles) }
// Redis client
single<RedisAsyncCommands<String, String>> {
RedisClient.create(Config.redis).connect()?.async() ?: error("Unable to connect to redis")
}
// RabbitMQ
single<ConnectionFactory> {
ConnectionFactory().apply { setUri(Config.rabbitmq) }
}
// JsonSerializer
single<ObjectMapper> {
jacksonObjectMapper().apply {
registerModule(SimpleModule())
propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
registerModule(JodaModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
}
}
// Client HTTP for WebSockets
single(named("ws")) {
HttpClient {
install(WebSockets)
}
}
// SQL Requester (postgresJson)
single {
Requester.RequesterFactory(
connection = get(),
functionsDirectory = Config.Sql.functionFiles
).createRequester()
}
// Repositories
single { UserRepository(get()) }
single { ArticleRepository(get()) }
single { CitizenRepository(get()) }
single { ConstitutionRepository(get()) }
single { FollowArticleRepository(get()) }
single { FollowConstitutionRepository(get()) }
single { CommentGenericRepository(get()) }
single { CommentArticleRepository(get()) }
single { CommentConstitutionRepository(get()) }
single { VoteArticleRepository(get()) }
single { VoteConstitutionRepository(get()) }
single { VoteCommentRepository(get()) }
single { OpinionChoiceRepository(get()) }
single { OpinionArticleRepository(get()) }
single { WorkgroupRepository(get()) }
// Elasticsearch Client
single<RestClient> {
RestClient.builder(
HttpHost.create(Config.elasticsearch)
).build()
}
single { ArticleViewManager(get()) }
// Mailler
single { Mailer(Config.sendGridKey) }
// SSO Manager for connection
single { SsoManager(get<Mailer>(), Config.domain, get()) }
single { Publisher(get(), get()) }
single { NotificationEmailSender(get<Mailer>(), Config.domain, get(), get()) }
}

View File

@@ -1,85 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import fr.postgresjson.entity.mutable.EntityVersioning
import fr.postgresjson.entity.mutable.UuidEntityVersioning
import java.util.*
class Article(
id: UUID = UUID.randomUUID(),
title: String,
override var anonymous: Boolean = true,
override var content: String,
override var description: String,
override var tags: List<String> = emptyList(),
draft: Boolean = false,
override var lastVersion: Boolean = false,
override val createdBy: CitizenBasic
) : ArticleFull,
ArticleAuthI<CitizenBasicI>,
ArticleSimple(id, title, createdBy, draft),
Viewable by ViewableImp() {
init {
tags = tags.distinct()
}
}
open class ArticleSimple(
id: UUID = UUID.randomUUID(),
override var title: String,
override val createdBy: CitizenBasic,
override var draft: Boolean = false
) : ArticleSimpleI,
ArticleAuthI<CitizenBasicI>,
ArticleRefVersioning(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp(),
Votable by VotableImp(),
Opinionable by OpinionableImp()
open class ArticleRefVersioning(
id: UUID = UUID.randomUUID(),
versionNumber: Int? = null,
versionId: UUID = UUID.randomUUID()
) : ArticleRef(id),
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionNumber, versionId)
open class ArticleRef(
id: UUID = UUID.randomUUID()
) : ArticleI, TargetRef(id)
interface ArticleI : UuidEntityI, TargetI
interface ArticleSimpleI :
ArticleI,
EntityVersioning<UUID, Int>,
EntityCreatedBy<CitizenBasicI>,
EntityCreatedAt,
EntityDeletedAt,
Votable {
var title: String
}
interface ArticleBasicI :
ArticleSimpleI {
var anonymous: Boolean
var content: String
var description: String
var tags: List<String>
}
interface ArticleFull :
ArticleBasicI {
var draft: Boolean
var lastVersion: Boolean
}
interface ArticleAuthI<U : CitizenWithUserI> :
ArticleI,
EntityCreatedBy<U>,
EntityDeletedAt {
var draft: Boolean
}

View File

@@ -1,80 +0,0 @@
package fr.dcproject.entity
import fr.dcproject.entity.CitizenI.Name
import fr.postgresjson.entity.immutable.EntityCreatedAt
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.immutable.UuidEntityI
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import org.joda.time.DateTime
import java.util.*
class Citizen(
id: UUID = UUID.randomUUID(),
name: Name,
email: String,
birthday: DateTime,
voteAnonymous: Boolean = true,
followAnonymous: Boolean = true,
override val user: User
) : CitizenFull,
CitizenBasic(id, name, email, birthday, voteAnonymous, followAnonymous, user),
EntityCreatedAt by EntityCreatedAtImp() {
var workgroups: List<WorkgroupSimple<CitizenRef>> = emptyList()
}
open class CitizenBasic(
id: UUID = UUID.randomUUID(),
name: Name,
override var email: String,
override var birthday: DateTime,
override var voteAnonymous: Boolean = true,
override var followAnonymous: Boolean = true,
override val user: User
) : CitizenBasicI,
CitizenSimple(id, name, user)
open class CitizenSimple(
id: UUID = UUID.randomUUID(),
var name: Name,
user: UserRef
) : CitizenRefWithUser(id, user)
open class CitizenRefWithUser(
id: UUID = UUID.randomUUID(),
override val user: UserRef
) : CitizenWithUserI,
CitizenRef(id),
EntityDeletedAt by EntityDeletedAtImp()
open class CitizenRef(
id: UUID = UUID.randomUUID()
) : UuidEntity(id),
CitizenI
interface CitizenI : UuidEntityI {
data class Name(
var firstName: String,
var lastName: String,
var civility: String? = null
) {
fun getFullName(): String = "${civility ?: ""} $firstName $lastName".trim()
}
}
interface CitizenBasicI : CitizenWithUserI, EntityDeletedAt {
var name: Name
var email: String
var birthday: DateTime
var voteAnonymous: Boolean
var followAnonymous: Boolean
}
interface CitizenFull : CitizenBasicI {
override val user: User
}
interface CitizenWithUserI : CitizenI {
val user: UserI
}

View File

@@ -1,39 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import java.util.*
open class Comment<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override var target: T,
var content: String,
val responses: List<Comment<T>>? = null,
var parent: Comment<T>? = null,
val parentsIds: List<UUID>? = null,
val childrenCount: Int? = null
) : ExtraI<T, CitizenBasicI>,
CommentRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
EntityUpdatedAt by EntityUpdatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp(),
Votable by VotableImp(),
TargetI {
constructor(
createdBy: CitizenBasic,
parent: Comment<T>,
content: String
) : this(
createdBy = createdBy,
parent = parent,
target = parent.target,
content = content
)
}
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentS(id)
sealed class CommentS(id: UUID) : TargetRef(id)

View File

@@ -1,69 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import java.util.*
class Constitution(
id: UUID = UUID.randomUUID(),
title: String,
anonymous: Boolean = true,
titles: MutableList<TitleSimple<ArticleSimple>> = mutableListOf(),
draft: Boolean = false,
lastVersion: Boolean = false,
override val createdBy: CitizenSimple
) : ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleSimple>>(
id,
title = title,
anonymous = anonymous,
titles = titles,
draft = draft,
lastVersion = lastVersion,
createdBy = createdBy
) {
class Title(
id: UUID = UUID.randomUUID(),
name: String,
rank: Int? = null,
override val articles: MutableList<ArticleSimple> = mutableListOf()
) : ConstitutionSimple.TitleSimple<ArticleSimple>(id, name, rank)
}
open class ConstitutionSimple<Cr : CitizenRefWithUser, T : ConstitutionSimple.TitleSimple<*>>(
id: UUID = UUID.randomUUID(),
var title: String,
var anonymous: Boolean = true,
open var titles: MutableList<T> = mutableListOf(),
var draft: Boolean = false,
var lastVersion: Boolean = false,
override val createdBy: Cr,
versionId: UUID = UUID.randomUUID()
) : ConstitutionRef(id),
EntityVersioning<UUID, Int?> by UuidEntityVersioning(versionId = versionId),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<Cr> by EntityCreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp() {
init {
titles.forEachIndexed { index, title ->
title.rank = index
}
}
open class TitleSimple<A : ArticleI>(
id: UUID = UUID.randomUUID(),
var name: String,
var rank: Int? = null,
open val articles: MutableList<A> = mutableListOf()
) : TitleRef(id)
}
open class ConstitutionRef(id: UUID = UUID.randomUUID()) : ConstitutionS(id) {
open class TitleRef(
id: UUID = UUID.randomUUID()
) : UuidEntity(id)
}
sealed class ConstitutionS(id: UUID = UUID.randomUUID()) : TargetRef(id), TargetI

View File

@@ -1,58 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.EntityCreatedAt
import fr.postgresjson.entity.immutable.EntityCreatedBy
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.immutable.UuidEntityI
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf
interface ExtraI<T : TargetI, C : CitizenI> :
UuidEntityI,
EntityCreatedAt,
EntityCreatedBy<C> {
val target: T
}
open class TargetRef(id: UUID = UUID.randomUUID(), reference: String = "") : TargetI, UuidEntity(id) {
final override val reference: String
get() = if (field != "") field else TargetI.getReference(this)
init {
this.reference = reference
}
}
interface TargetI : UuidEntityI {
enum class TargetName(val targetReference: String) {
Article("article"),
Constitution("constitution"),
Comment("comment"),
Opinion("opinion")
}
companion object {
fun <T : TargetI> getReference(t: KClass<T>): String {
return when {
t.isSubclassOf(ArticleRef::class) -> TargetName.Article.targetReference
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(Opinion::class) -> TargetName.Opinion.targetReference
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
}
}
fun getReference(t: TargetI): String {
val ref = this.getReference(t::class)
return if (t is ExtraI<*, *>) {
"${ref}_on_${t.target.reference}"
} else {
ref
}
}
}
val reference: String
}

View File

@@ -1,20 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import java.util.*
class Follow<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override var target: T
) : ExtraI<T, CitizenBasicI>,
FollowSimple<T, CitizenBasicI>(id, createdBy, target)
open class FollowSimple<T : TargetI, C : CitizenI>(
id: UUID = UUID.randomUUID(),
override val createdBy: C,
override var target: T
) : ExtraI<T, C>,
UuidEntity(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<C> by EntityCreatedByImp(createdBy)

View File

@@ -1,27 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.EntityCreatedAt
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
import fr.postgresjson.entity.immutable.EntityCreatedBy
import fr.postgresjson.entity.immutable.EntityCreatedByImp
import java.util.*
open class Opinion<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override val target: T,
val choice: OpinionChoice
) : ExtraI<T, CitizenBasicI>,
TargetRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy) {
fun getName(): String = choice.name
}
class OpinionArticle(
id: UUID = UUID.randomUUID(),
createdBy: CitizenBasic,
target: ArticleRef,
choice: OpinionChoice
) : Opinion<ArticleRef>(id, createdBy, target, choice)

View File

@@ -1,20 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.EntityCreatedAt
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import java.util.*
class OpinionChoice(
id: UUID? = null,
val name: String,
val target: List<String>?
) : OpinionChoiceRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp()
open class OpinionChoiceRef(
id: UUID?
) : UuidEntity(id ?: UUID.randomUUID())

View File

@@ -1,15 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
class OpinionAggregation(
private val underlying: MutableMap<String, Any> = mutableMapOf()
) : MutableMap<String, Any> by underlying, EntityI
interface Opinionable {
var opinions: MutableMap<String, Int>
}
class OpinionableImp : Opinionable {
override var opinions: MutableMap<String, Int> = mutableMapOf()
}

View File

@@ -1,41 +0,0 @@
package fr.dcproject.entity
import fr.dcproject.entity.UserI.Roles
import fr.postgresjson.entity.immutable.*
import io.ktor.auth.Principal
import org.joda.time.DateTime
import java.util.*
class User(
id: UUID = UUID.randomUUID(),
username: String,
blockedAt: DateTime? = null,
override var plainPassword: String? = null,
override var roles: List<Roles> = emptyList()
) : UserFull, UserBasic(id, username, blockedAt),
EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp()
open class UserBasic(
id: UUID = UUID.randomUUID(),
override var username: String,
override var blockedAt: DateTime? = null
) : UserBasicI, UserRef(id)
open class UserRef(
id: UUID = UUID.randomUUID()
) : UserI, UuidEntity(id)
interface UserI : UuidEntityI, Principal {
enum class Roles { ROLE_USER, ROLE_ADMIN }
}
interface UserBasicI : UserI {
var username: String
var blockedAt: DateTime?
}
interface UserFull : UserBasicI, EntityCreatedAt, EntityUpdatedAt {
var plainPassword: String?
var roles: List<Roles>
}

View File

@@ -1,13 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
import fr.postgresjson.entity.immutable.EntityUpdatedAt
import fr.postgresjson.entity.immutable.EntityUpdatedAtImp
open class ViewAggregation(
val total: Int,
val unique: Int
) : EntityI,
EntityUpdatedAt by EntityUpdatedAtImp() {
constructor() : this(0, 0)
}

View File

@@ -1,9 +0,0 @@
package fr.dcproject.entity
interface Viewable {
var views: ViewAggregation
}
class ViewableImp : Viewable {
override var views: ViewAggregation = ViewAggregation()
}

View File

@@ -1,9 +0,0 @@
package fr.dcproject.entity
interface Votable {
var votes: VoteAggregation
}
class VotableImp : Votable {
override var votes: VoteAggregation = VoteAggregation()
}

View File

@@ -1,22 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import java.util.*
open class Vote<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override var target: T,
var note: Int,
var anonymous: Boolean = true
) : ExtraI<T, CitizenBasicI>,
UuidEntity(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
EntityUpdatedAt by EntityUpdatedAtImp() {
init {
if (note > 1 && note < -1) {
error("note must be 1, 0 or -1")
}
}
}

View File

@@ -1,16 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
import fr.postgresjson.entity.mutable.EntityUpdatedAt
import fr.postgresjson.entity.mutable.EntityUpdatedAtImp
open class VoteAggregation(
val up: Int,
val neutral: Int,
val down: Int,
val total: Int,
val score: Int
) : EntityI,
EntityUpdatedAt by EntityUpdatedAtImp() {
constructor() : this(0, 0, 0, 0, 0)
}

View File

@@ -1,63 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import java.util.*
class Workgroup(
id: UUID? = null,
name: String,
description: String,
logo: String? = null,
anonymous: Boolean = true,
owner: CitizenBasic,
createdBy: CitizenBasic,
override var members: List<CitizenBasic> = emptyList()
) : WorkgroupWithAuthI<CitizenBasic>,
WorkgroupSimple<CitizenBasic>(
id,
name,
description,
logo,
anonymous,
owner,
createdBy
),
EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp()
open class WorkgroupSimple<Z : CitizenRef>(
id: UUID? = null,
var name: String,
var description: String,
var logo: String? = null,
var anonymous: Boolean = true,
var owner: Z,
createdBy: Z
) : WorkgroupRef(id),
EntityCreatedBy<Z> by EntityCreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp()
open class WorkgroupRef(
id: UUID? = null
) : UuidEntity(id ?: UUID.randomUUID()), WorkgroupI
interface WorkgroupWithAuthI<Z : CitizenWithUserI> : WorkgroupWithMembersI<Z>, EntityCreatedBy<Z>, EntityDeletedAt {
val anonymous: Boolean
val owner: Z
fun isMember(user: UserI): Boolean =
members.map { it.user.id }.contains(user.id) || owner.user.id == user.id
fun isMember(citizen: CitizenWithUserI): Boolean =
isMember(citizen.user)
}
interface WorkgroupWithMembersI<Z : CitizenI> : WorkgroupI {
var members: List<Z>
}
fun List<CitizenI>.asCitizen(citizen: CitizenI): Boolean = this.map { it.id }.contains(citizen.id)
interface WorkgroupI : UuidEntityI

View File

@@ -1,133 +0,0 @@
package fr.dcproject.event
import com.rabbitmq.client.*
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import fr.dcproject.Config
import fr.dcproject.entity.Article
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.FollowSimple
import fr.dcproject.entity.TargetRef
import fr.dcproject.event.publisher.Publisher
import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.repository.Follow
import fr.postgresjson.serializer.deserialize
import io.ktor.application.ApplicationCall
import io.ktor.application.EventDefinition
import io.ktor.application.application
import io.ktor.util.pipeline.PipelineContext
import io.lettuce.core.api.async.RedisAsyncCommands
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.io.errors.IOException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
class ArticleUpdate(
target: Article
) : EntityEvent(target, "article", "update") {
companion object {
val event = EventDefinition<ArticleUpdate>()
}
}
fun <T : Event> PipelineContext<Unit, ApplicationCall>.raiseEvent(definition: EventDefinition<T>, value: T) =
application.environment.monitor.raise(definition, value)
class EventNotification(
private val config: EventSubscriber.Configuration,
private val rabbitFactory: ConnectionFactory,
private val redis: RedisAsyncCommands<String, String>,
private val followRepo: FollowArticleRepository,
private val publisher: Publisher,
private val notificationEmailSender: NotificationEmailSender
) {
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
fun config() {
/* Config Rabbit */
val exchangeName = Config.exchangeNotificationName
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, "")
}
}
/* Declare publisher on event */
config.subscribe(ArticleUpdate.event) {
publisher.publish(it)
}
/* Launch Consumer */
GlobalScope.launch {
val rabbitChannel = rabbitFactory.newConnection().createChannel()
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: AMQP.BasicProperties,
body: ByteArray
) = runBlocking {
decodeEvent(body) {
redis.zadd(
"notification:${follow.createdBy.id}",
event.id,
rawEvent
)
}
rabbitChannel.basicAck(envelope.deliveryTag, false)
}
}
val consumerEmail: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: AMQP.BasicProperties,
body: ByteArray
) {
runBlocking {
decodeEvent(body) {
logger.debug("EmailSend to: ${follow.createdBy.id}")
notificationEmailSender.sendEmail(follow)
}
}
rabbitChannel.basicAck(envelope.deliveryTag, false)
}
}
rabbitChannel.basicConsume("push", false, consumerPush) // The front consume the redis via Websocket
rabbitChannel.basicConsume("email", false, consumerEmail)
}
}
private suspend fun decodeEvent(body: ByteArray, action: suspend Msg.() -> Unit) {
val rawEvent = body.toString(Charsets.UTF_8)
val event = rawEvent.deserialize<EntityEvent>() ?: error("Unable to unserialise event message from rabbit")
val repo = when (event.type) {
"article" -> followRepo
else -> error("event '${event.type}' not implemented")
} as Follow<*, *>
repo
.findFollowsByTarget(event.target)
.collect {
Msg(event, rawEvent, it).action()
}
}
private class Msg(
val event: EntityEvent,
val rawEvent: String,
val follow: FollowSimple<out TargetRef, CitizenRef>
)
}

View File

@@ -1,54 +0,0 @@
package fr.dcproject.event
import fr.postgresjson.entity.Serializable
import fr.postgresjson.entity.immutable.UuidEntity
import io.ktor.application.*
import io.ktor.util.AttributeKey
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.DisposableHandle
import org.joda.time.DateTime
import kotlin.random.Random.Default.nextInt
open class Event(
val type: String,
val createdAt: DateTime = DateTime.now()
) : Serializable {
val id: Double = randId(createdAt.millis)
private fun randId(time: Long): Double {
return (time.toString() + nextInt(1000, 9999).toString()).toDouble()
}
}
open class EntityEvent(
val target: UuidEntity,
type: String,
val action: String
) : Event(type)
/**
* Installation Class
*/
class EventSubscriber {
class Configuration(private val monitor: ApplicationEvents) {
private val subscribers = mutableListOf<DisposableHandle>()
fun <T : Event> subscribe(definition: EventDefinition<T>, handler: EventHandler<T>): DisposableHandle {
return monitor.subscribe(definition, handler).also {
subscribers.add(it)
}
}
}
companion object Feature : ApplicationFeature<Application, Configuration, EventSubscriber> {
override val key = AttributeKey<EventSubscriber>("EventSubscriber")
@KtorExperimentalAPI
override fun install(
pipeline: Application,
configure: Configuration.() -> Unit
): EventSubscriber {
Configuration(pipeline.environment.monitor).apply(configure)
return EventSubscriber()
}
}
}

View File

@@ -1,32 +0,0 @@
package fr.dcproject.event.publisher
import com.fasterxml.jackson.databind.ObjectMapper
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.Config
import fr.dcproject.event.EntityEvent
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class Publisher(
private val mapper: ObjectMapper,
private val factory: ConnectionFactory,
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
) {
fun <T : EntityEvent> publish(it: T): Job {
return GlobalScope.launch {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->
channel.basicPublish(Config.exchangeNotificationName, "", null, it.serialize().toByteArray())
logger.debug("Publish message ${it.target.id}")
}
}
}
}
private fun EntityEvent.serialize(): String {
return mapper.writeValueAsString(this) ?: error("Unable tu serialize message")
}
}

View File

@@ -0,0 +1,88 @@
package fr.dcproject
import com.fasterxml.jackson.core.util.DefaultIndenter
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.joda.JodaModule
import fr.dcproject.entity.Article
import fr.dcproject.routes.article
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.features.AutoHeadResponse
import io.ktor.features.ContentNegotiation
import io.ktor.features.DataConversion
import io.ktor.jackson.jackson
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations
import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import org.koin.ktor.ext.Koin
import org.koin.ktor.ext.get
import java.util.*
import fr.dcproject.repository.Article as RepositoryArticle
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module() {
install(Koin) {
// Slf4jLog()
modules(Module)
}
install(DataConversion) {
convert<UUID> {
decode { values, _ ->
values.singleOrNull()?.let { UUID.fromString(it) }
}
encode { value ->
when (value) {
null -> listOf()
is UUID -> listOf(value.toString())
else -> throw InternalError("Cannot convert $value as UUID")
}
}
}
convert<Article> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<RepositoryArticle>().findById(id) ?: throw InternalError("Article $values not found")
}
}
}
install(Locations) {
}
install(Authentication) {
}
install(AutoHeadResponse)
install(ContentNegotiation) {
// TODO move to postgresJson lib
jackson {
propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
registerModule(JodaModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
configure(SerializationFeature.INDENT_OUTPUT, true)
setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
indentObjectsWith(DefaultIndenter(" ", "\n"))
})
}
}
install(Routing) {
article(get())
}
}

View File

@@ -0,0 +1,16 @@
package fr.dcproject
import com.typesafe.config.ConfigFactory
import java.io.File
class Config {
private var config = ConfigFactory.load()
val sqlFiles = File(this::class.java.getResource("/sql").toURI())
val envName: String = config.getString("app.envName")
val host: String = config.getString("db.host")
var database: String = config.getString("db.database")
var username: String = config.getString("db.username")
var password: String = config.getString("db.password")
val port: Int = config.getInt("db.port")
}

View File

@@ -0,0 +1,26 @@
package fr.dcproject
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
import io.ktor.util.KtorExperimentalAPI
import org.koin.dsl.module
import fr.dcproject.repository.Article as ArticleRepository
val config = Config()
@KtorExperimentalAPI
val Module = module {
single { config }
single { Connection(host = config.host, port = config.port, database = config.database, username = config.username, password = config.password) }
single { Requester.RequesterFactory(
connection = get(),
functionsDirectory = config.sqlFiles.resolve("functions")
).createRequester() }
single { ArticleRepository(get()) }
single { Migrations(connection = get(), directory = config.sqlFiles) }
}

View File

@@ -0,0 +1,19 @@
package fr.dcproject.entity
import fr.postgresjson.entity.*
import java.util.*
class Article(
id: UUID = UUID.randomUUID(),
var versionId: UUID = UUID.randomUUID(),
var versionNumber: Int? = null,
var title: String?,
var annonymous: Boolean? = true,
var content: String?,
var description: String?,
var tags: List<String> = emptyList(),
override var createdBy: Citizen?
):
UuidEntity(id),
EntityCreatedAt by EntityCreatedAtImp(),
CreatedBy<Citizen> by EntityCreatedByImp()

View File

@@ -0,0 +1,25 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.UuidEntity
import org.joda.time.DateTime
import java.util.*
class Citizen(
id: UUID = UUID.randomUUID(),
var name: Name?,
var birthday: DateTime?,
var userId: String? = null,
var voteAnnonymous: Boolean? = null,
var followAnnonymous: Boolean? = null,
var user: User?
) : UuidEntity(id),
EntityCreatedAt by EntityCreatedAtImp() {
data class Name(
var firstName: String?,
var lastName: String?,
var civility: String? = null
)
}

View File

@@ -0,0 +1,14 @@
package fr.dcproject.entity
import fr.postgresjson.entity.*
import org.joda.time.DateTime
import java.util.*
class User(
id: UUID? = UUID.randomUUID(),
var username: String?,
var blockedAt: DateTime? = null,
var plainPassword: String?
) : UuidEntity(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp()

View File

@@ -0,0 +1,53 @@
package fr.dcproject.repository
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.EntitiesCollections
import fr.postgresjson.repository.RepositoryI
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
class Article(override var requester: Requester) : RepositoryI<ArticleEntity> {
override val entityName = ArticleEntity::class
fun findById(id: UUID): ArticleEntity? {
val function = requester.getFunction("find_article_by_id")
return when (val e = EntitiesCollections().get(id) as ArticleEntity?) {
null -> {
function.selectOne("id" to id)
}
else -> e
}
}
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: Direction? = null,
search: String? = null
): Paginated<ArticleEntity> {
return requester
.getFunction("find_articles")
.select(
page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search
)
}
fun upsert(article: ArticleEntity): ArticleEntity? {
return requester
.getFunction("upsert_article")
.selectOne<ArticleEntity>("resource" to article)?.also {
EntitiesCollections().set(it)
}
}
enum class Direction {
asc,
desc
}
}

View File

@@ -0,0 +1,30 @@
package fr.dcproject.routes
import Paths
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.repository.Article as ArticleRepository
@KtorExperimentalLocationsAPI
fun Route.article(repo: ArticleRepository) {
get<Paths.ArticlesRequest> {
val articles = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
call.respond(articles)
}
get<Paths.ArticleRequest> {
call.respond(it.article)
}
post<Paths.PostArticleRequest>() {
val article = call.receive<ArticleEntity>()
repo.upsert(article)
call.respond(article)
}
}

View File

@@ -0,0 +1,14 @@
import fr.dcproject.entity.Article
import fr.dcproject.repository.Article.Direction
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
@KtorExperimentalLocationsAPI
object Paths {
@Location("/articles") class ArticlesRequest(page: Int = 1, limit: Int = 50, val sort: String? = null, val direction: 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
}
@Location("/articles/{article}") class ArticleRequest(val article: Article)
@Location("/articles") class PostArticleRequest
}

View File

@@ -5,6 +5,6 @@ import org.slf4j.LoggerFactory
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
internal class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> { internal class LoggerDelegate<in R: Any>: ReadOnlyProperty<R, Logger> {
override fun getValue(thisRef: R, property: KProperty<*>) = LoggerFactory.getLogger(thisRef.javaClass.packageName) override fun getValue(thisRef: R, property: KProperty<*>) = LoggerFactory.getLogger(thisRef.javaClass.packageName)
} }

View File

@@ -1,27 +0,0 @@
package fr.dcproject.messages
import com.sendgrid.Method
import com.sendgrid.Request
import com.sendgrid.SendGrid
import com.sendgrid.helpers.mail.Mail
import java.io.IOException
class Mailer(
private val key: String
) {
fun sendEmail(action: () -> Mail): Boolean {
val mail = action()
val sg = SendGrid(key)
val request = Request()
try {
request.method = Method.POST
request.endpoint = "mail/send"
request.body = mail.build()
val response = sg.api(request)
return response.statusCode == 202
} catch (ex: IOException) {
throw ex
}
}
}

View File

@@ -1,66 +0,0 @@
package fr.dcproject.messages
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.entity.*
import fr.postgresjson.entity.immutable.UuidEntityI
import java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Article as ArticleRepository
class NotificationEmailSender(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository,
private val articleRepo: ArticleRepository
) {
fun sendEmail(follow: FollowSimple<out TargetRef, CitizenRef>) {
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: CitizenBasicI, target: UuidEntityI): String? {
return when (target) {
is Article -> """
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: CitizenBasicI, target: UuidEntityI): String {
return when (target) {
is Article -> """
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,51 +0,0 @@
package fr.dcproject.messages
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.JwtConfig
import fr.dcproject.entity.CitizenBasicI
import io.ktor.http.URLBuilder
import fr.dcproject.repository.Citizen as CitizenRepository
class SsoManager(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository
) {
fun sendEmail(email: String, url: String) {
val citizen = citizenRepo.findByEmail(email) ?: noEmail(email)
sendEmail(citizen, url)
}
fun sendEmail(citizen: CitizenBasicI, url: String) {
mailer.sendEmail {
Mail(
Email("sso@$domain"),
"Connection",
Email(citizen.email),
Content("text/plain", generateContent(citizen, url))
).apply {
addContent(Content("text/html", generateHtmlContent(citizen, url)))
}
}
}
private fun generateHtmlContent(citizen: CitizenBasicI, url: String): String? {
val urlObject = URLBuilder(url)
urlObject.parameters.append("token", JwtConfig.makeToken(citizen.user))
return "Click <a href=\"${urlObject.buildString()}\">here</a> for connect to $domain"
}
private fun generateContent(citizen: CitizenBasicI, url: String): String {
val urlObject = URLBuilder(url)
urlObject.parameters.append("token", JwtConfig.makeToken(citizen.user))
return "Copy this link into your browser for connect to $domain: \n${urlObject.buildString()}"
}
class EmailNotFound(val email: String) : Exception() {
override val message: String = "No Citizen with this email : $email"
}
private fun noEmail(email: String): Nothing = throw EmailNotFound(email)
}

View File

@@ -1,54 +0,0 @@
package fr.dcproject.repository
import fr.dcproject.entity.ArticleFull
import fr.dcproject.entity.ArticleSimple
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter
import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
class Article(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): ArticleEntity? {
val function = requester.getFunction("find_article_by_id")
return function.selectOne("id" to id)
}
fun findVerionsByVersionsId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleEntity> {
return requester
.getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to versionId)
}
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: Direction? = null,
search: String? = null,
filter: Filter = Filter()
): Paginated<ArticleSimple> {
return requester
.getFunction("find_articles")
.select(
page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search,
"filter" to filter
)
}
fun upsert(article: ArticleFull): ArticleEntity? {
return requester
.getFunction("upsert_article")
.selectOne("resource" to article)
}
class Filter(
val createdById: String? = null
) : Parameter
}

View File

@@ -1,67 +0,0 @@
package fr.dcproject.repository
import fr.dcproject.entity.CitizenBasic
import fr.dcproject.entity.CitizenFull
import fr.dcproject.entity.UserI
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
class Citizen(override var requester: Requester) : RepositoryI {
fun findById(id: UUID, withUser: Boolean = false): CitizenEntity? {
return requester
.getFunction(if (withUser) "find_citizen_by_id_with_user" else "find_citizen_by_id")
.selectOne("id" to id)
}
fun findByUser(user: UserI): CitizenEntity? {
return requester
.getFunction("find_citizen_by_user_id")
.selectOne("user_id" to user.id)
}
fun findByUsername(unsername: String): CitizenEntity? {
return requester
.getFunction("find_citizen_by_username")
.selectOne("username" to unsername)
}
fun findByEmail(email: String): CitizenEntity? {
return requester
.getFunction("find_citizen_by_email")
.selectOne("email" to email)
}
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: Direction? = null,
search: String? = null
): Paginated<CitizenBasic> {
return requester
.getFunction("find_citizens")
.select(
page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search
)
}
fun upsert(citizen: CitizenFull): CitizenEntity? {
return requester
.getFunction("upsert_citizen")
.selectOne("resource" to citizen)
}
fun insertWithUser(citizen: CitizenFull): CitizenEntity? {
return requester
.getFunction("insert_citizen_with_user")
.selectOne("resource" to citizen)
}
}

View File

@@ -1,210 +0,0 @@
package fr.dcproject.repository
import fr.dcproject.entity.ArticleRef
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.immutable.UuidEntityI
import fr.postgresjson.repository.RepositoryI
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.entity.Article as ArticleEntity
abstract class Comment<T : TargetI>(override var requester: Requester) : RepositoryI {
abstract fun findById(id: UUID): CommentEntity<T>?
abstract fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<CommentEntity<T>>
open fun findByParent(
parent: CommentEntity<T>,
page: Int = 1,
limit: Int = 50
): Paginated<CommentEntity<T>> {
return findByParent(parent.id, page, limit)
}
open fun findByParent(
parentId: UUID,
page: Int = 1,
limit: Int = 50
): Paginated<CommentEntity<T>> {
return requester.run {
getFunction("find_comments_by_parent")
.select(
page, limit,
"parent_id" to parentId
)
}
}
open fun findByTarget(
target: UuidEntityI,
page: Int = 1,
limit: Int = 50,
sort: CommentArticle.Sort = CommentArticle.Sort.CREATED_AT
): Paginated<CommentEntity<T>> {
return findByTarget(target.id, page, limit, sort)
}
open fun findByTarget(
targetId: UUID,
page: Int = 1,
limit: Int = 50,
sort: CommentArticle.Sort = CommentArticle.Sort.CREATED_AT
): Paginated<CommentEntity<T>> {
return requester.run {
getFunction("find_comments_by_target")
.select(
page, limit,
"target_id" to targetId,
"sort" to sort.sql
)
}
}
fun <I : T> comment(comment: CommentEntity<I>) {
requester
.getFunction("comment")
.sendQuery(
"reference" to comment.target.reference,
"resource" to comment
)
}
fun <I : T> edit(comment: CommentEntity<I>) {
requester
.getFunction("edit_comment")
.sendQuery(
"id" to comment.id,
"content" to comment.content
)
}
}
class CommentGeneric(requester: Requester) : Comment<TargetRef>(requester) {
override fun findById(id: UUID): CommentEntity<TargetRef>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
page: Int,
limit: Int
): Paginated<CommentEntity<TargetRef>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(
page, limit,
"created_by_id" to citizen.id
)
}
}
override fun findByParent(
parentId: UUID,
page: Int,
limit: Int
): Paginated<CommentEntity<TargetRef>> {
return requester.run {
getFunction("find_comments_by_parent")
.select(
page, limit,
"parent_id" to parentId
)
}
}
}
class CommentArticle(requester: Requester) : Comment<ArticleEntity>(requester) {
override fun findById(id: UUID): CommentEntity<ArticleEntity>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
page: Int,
limit: Int
): Paginated<CommentEntity<ArticleEntity>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(
page, limit,
"created_by_id" to citizen.id,
"reference" to TargetI.getReference(ArticleRef::class)
)
}
}
override fun findByTarget(
target: UuidEntityI,
page: Int,
limit: Int,
sort: Sort
): Paginated<CommentEntity<ArticleEntity>> = requester
.getFunction("find_comments_by_target")
.select(
page, limit,
"target_id" to target.id,
"sort" to sort.sql
)
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 }
}
}
}
}
class CommentConstitution(requester: Requester) : Comment<ConstitutionRef>(requester) {
override fun findById(id: UUID): CommentEntity<ConstitutionRef>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
page: Int,
limit: Int
): Paginated<CommentEntity<ConstitutionRef>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(
page, limit,
"created_by_id" to citizen.id,
"reference" to TargetI.getReference(ConstitutionRef::class)
)
}
}
override fun findByTarget(
target: UuidEntityI,
page: Int,
limit: Int,
sort: CommentArticle.Sort
): Paginated<CommentEntity<ConstitutionRef>> {
return requester.run {
getFunction("find_comments_by_target")
.select(
page, limit,
"target_id" to target.id,
"sort" to sort.sql
)
}
}
}

View File

@@ -1,42 +0,0 @@
package fr.dcproject.repository
import fr.dcproject.entity.ArticleRef
import fr.dcproject.entity.CitizenSimple
import fr.dcproject.entity.ConstitutionSimple
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Constitution as ConstitutionEntity
class Constitution(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): ConstitutionEntity? {
val function = requester.getFunction("find_constitution_by_id")
return function.selectOne("id" to id)
}
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: Direction? = null,
search: String? = null
): Paginated<ConstitutionEntity> {
return requester
.getFunction("find_constitutions")
.select(
page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search
)
}
fun upsert(constitution: ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleRef>>): ConstitutionEntity? {
return requester
.getFunction("upsert_constitution")
.selectOne("resource" to constitution)
}
}

View File

@@ -1,140 +0,0 @@
package fr.dcproject.repository
import fr.dcproject.entity.*
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.repository.RepositoryI
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Constitution as ConstitutionEntity
import fr.dcproject.entity.Follow as FollowEntity
sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Requester) : RepositoryI {
open fun findByCitizen(
citizen: CitizenI,
page: Int = 1,
limit: Int = 50
): Paginated<FollowEntity<OUT>> =
findByCitizen(citizen.id, page, limit)
open fun findByCitizen(
citizenId: UUID,
page: Int = 1,
limit: Int = 50
): Paginated<FollowEntity<OUT>> {
return requester
.getFunction("find_follows_by_citizen")
.select(
page, limit,
"created_by_id" to citizenId
)
}
fun follow(follow: FollowEntity<IN>) {
requester
.getFunction("follow")
.sendQuery(
"reference" to follow.target.reference,
"target_id" to follow.target.id,
"created_by_id" to follow.createdBy.id
)
}
fun unfollow(follow: FollowEntity<IN>) {
requester
.getFunction("unfollow")
.sendQuery(
"reference" to follow.target.reference,
"target_id" to follow.target.id,
"created_by_id" to follow.createdBy.id
)
}
open fun findFollow(
citizen: CitizenI,
target: TargetRef
): FollowEntity<OUT>? =
requester
.getFunction("find_follow")
.selectOne(
"citizen_id" to citizen.id,
"target_id" to target.id,
"target_reference" to target.reference
)
fun findFollowsByTarget(
target: UuidEntity,
bulkSize: Int = 300
): Flow<FollowSimple<IN, CitizenRef>> = flow {
var nextPage = 1
do {
val paginate = findFollowsByTarget(target, nextPage, bulkSize)
paginate.result.forEach {
emit(it)
}
nextPage = paginate.currentPage + 1
} while (!paginate.isLastPage())
}
abstract fun findFollowsByTarget(
target: UuidEntity,
page: Int = 1,
limit: Int = 300
): Paginated<FollowSimple<IN, CitizenRef>>
}
class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleEntity>(requester) {
override fun findByCitizen(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowEntity<ArticleEntity>> {
return requester.run {
getFunction("find_follows_article_by_citizen")
.select(
page, limit,
"created_by_id" to citizenId
)
}
}
override fun findFollowsByTarget(
target: UuidEntity,
page: Int,
limit: Int
): Paginated<FollowSimple<ArticleRef, CitizenRef>> {
return requester
.getFunction("find_follows_article_by_target")
.select(
page, limit,
"target_id" to target.id
)
}
}
class FollowConstitution(requester: Requester) : Follow<ConstitutionRef, ConstitutionEntity>(requester) {
override fun findByCitizen(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowEntity<ConstitutionEntity>> {
return requester.run {
getFunction("find_follows_constitution_by_citizen")
.select(
page, limit,
"created_by_id" to citizenId
)
}
}
override fun findFollowsByTarget(
target: UuidEntity,
page: Int,
limit: Int
): Paginated<FollowSimple<ConstitutionRef, CitizenRef>> {
TODO("Not yet implemented")
}
}

View File

@@ -1,156 +0,0 @@
package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.entity.ArticleRef
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Opinion as OpinionEntity
import fr.dcproject.entity.OpinionArticle as OpinionArticleEntity
import fr.dcproject.entity.OpinionChoice as OpinionChoiceEntity
open class OpinionChoice(override val requester: Requester) : RepositoryI {
/**
* find all opinion choices
* can be filtered by target compatibility
*/
fun findOpinionsChoices(targets: List<String> = emptyList()): List<OpinionChoiceEntity> =
requester
.getFunction("find_opinion_choices")
.select(
"targets" to targets
)
/**
* find opinion choices by name
*/
fun findOpinionsChoiceByName(name: String): OpinionChoiceEntity? =
findOpinionsChoices().first {
it.name == name
}
/**
* find one opinion choices by id
*/
fun findOpinionChoiceById(id: UUID): OpinionChoiceEntity? =
requester
.getFunction("find_opinion_choice_by_id")
.selectOne(
"id" to id
)
/**
* find one opinion choices by id
*/
fun findOpinionChoicesByIds(ids: List<UUID>): List<OpinionChoiceEntity> =
requester
.getFunction("find_opinion_choices_by_ids")
.select(
"ids" to ids
)
fun upsertOpinionChoice(opinionChoice: OpinionChoiceEntity): OpinionChoiceEntity = requester
.getFunction("upsert_opinion_choice")
.selectOne(
"resource" to opinionChoice
)!!
}
abstract class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requester) {
/**
* Create an Opinion on target (article,...)
*/
abstract fun updateOpinions(choices: List<OpinionChoiceRef>, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>>
fun updateOpinions(choice: OpinionChoiceRef, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>> =
updateOpinions(listOf(choice), citizen, target)
abstract fun addOpinion(opinion: OpinionEntity<T>): OpinionEntity<T>
/**
* Find opinions of one citizen filtered by target ids
*/
fun findCitizenOpinionsByTargets(
citizen: CitizenEntity,
targets: List<UUID>
): List<OpinionEntity<T>> {
val typeReference = object : TypeReference<List<OpinionEntity<T>>>() {}
return requester.run {
getFunction("find_citizen_opinions_by_target_ids")
.select(
typeReference, mapOf(
"citizen_id" to citizen.id,
"ids" to targets
)
)
}
}
/**
* find opinion of citizen filtered by one target id
*/
fun findCitizenOpinionsByTarget(
citizen: CitizenEntity,
target: UUID
): List<OpinionEntity<T>> {
val typeReference = object : TypeReference<List<OpinionEntity<T>>>() {}
return requester
.getFunction("find_citizen_opinions_by_target_id")
.select(
typeReference, mapOf(
"citizen_id" to citizen.id,
"id" to target
)
)
}
/**
* find paginated opinion of one citizen
* can be sorted
*/
fun findCitizenOpinions(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: RepositoryI.Direction? = null
): Paginated<OpinionEntity<TargetRef>> {
return requester
.getFunction("find_citizen_opinions")
.select(page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"citizen_id" to citizen.id
)
}
}
class OpinionArticle(requester: Requester) : Opinion<ArticleRef>(requester) {
/**
* Update Opinions on Article (Delete old one)
*/
override fun updateOpinions(choices: List<OpinionChoiceRef>, citizen: CitizenRef, target: TargetRef): List<OpinionArticleEntity> {
return requester
.getFunction("update_citizen_opinions_by_target_id")
.select(
"choices_ids" to choices.map { it.id },
"citizen_id" to citizen.id,
"target_id" to target.id,
"target_reference" to target.reference
)
}
/**
* Add Opinions on Article
*/
override fun addOpinion(opinion: OpinionEntity<ArticleRef>): OpinionArticleEntity {
return requester
.getFunction("upsert_opinion")
.selectOne("resource" to opinion)!!
}
}

View File

@@ -1,43 +0,0 @@
package fr.dcproject.repository
import fr.dcproject.entity.UserFull
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import io.ktor.auth.UserPasswordCredential
import java.util.*
import fr.dcproject.entity.User as UserEntity
class User(override var requester: Requester) : RepositoryI {
fun findByCredentials(credentials: UserPasswordCredential): UserEntity? {
return requester
.getFunction("check_user")
.selectOne(
"username" to credentials.name,
"plain_password" to credentials.password
)
}
fun findById(id: UUID): UserEntity {
return requester
.getFunction("find_user_by_id")
.selectOne(
"id" to id
) ?: throw UserNotFound(id)
}
fun insert(user: UserEntity): UserEntity? {
return requester
.getFunction("insert_user")
.selectOne("resource" to user)
}
fun changePassword(user: UserFull) {
requester
.getFunction("change_user_password")
.sendQuery("resource" to user)
}
class UserNotFound(override val message: String?, override val cause: Throwable?) : Throwable(message, cause) {
constructor(id: UUID) : this("No User with ID $id", null)
}
}

View File

@@ -1,123 +0,0 @@
package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.entity.*
import fr.dcproject.entity.Article
import fr.dcproject.entity.Comment
import fr.dcproject.entity.Constitution
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Vote as VoteEntity
open class Vote<T : TargetI>(override var requester: Requester) : RepositoryI {
fun vote(vote: VoteEntity<T>): VoteAggregation {
val author = vote.createdBy
val anonymous = author.voteAnonymous
return requester
.getFunction("vote")
.selectOne(
"reference" to vote.target.reference,
"target_id" to vote.target.id,
"note" to vote.note,
"created_by_id" to author.id,
"anonymous" to anonymous
)!!
}
fun findByCitizen(
citizenId: UUID,
target: String,
typeReference: TypeReference<List<VoteEntity<T>>>,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<T>> {
return requester.run {
getFunction("find_votes_by_citizen")
.select(
page, limit, typeReference, mapOf(
"created_by_id" to citizenId,
"reference" to target
)
)
}
}
fun findCitizenVotesByTargets(
citizen: CitizenEntity,
targets: List<UUID>
): List<VoteEntity<*>> {
val typeReference = object : TypeReference<List<VoteEntity<TargetRef>>>() {}
return requester.run {
getFunction("find_citizen_votes_by_target_ids")
.select(
typeReference, mapOf(
"citizen_id" to citizen.id,
"ids" to targets
)
)
}
}
}
class VoteArticle(requester: Requester) : Vote<Article>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<Article>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<Article>>>() {},
page,
limit
)
}
class VoteArticleComment(requester: Requester) : Vote<Comment<Article>>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<Comment<Article>>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<Comment<Article>>>>() {},
page,
limit
)
}
class VoteComment(requester: Requester) : Vote<Comment<TargetRef>>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<Comment<TargetRef>>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<Comment<TargetRef>>>>() {},
page,
limit
)
}
class VoteConstitution(requester: Requester) : Vote<Constitution>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<Constitution>> =
findByCitizen(
citizen.id,
"constitution",
object : TypeReference<List<VoteEntity<Constitution>>>() {},
page,
limit
)
}

View File

@@ -1,85 +0,0 @@
package fr.dcproject.repository
import fr.dcproject.entity.*
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter
import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import fr.postgresjson.serializer.serialize
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Workgroup as WorkgroupEntity
class Workgroup(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): WorkgroupEntity? {
val function = requester.getFunction("find_workgroup_by_id")
return function.selectOne("id" to id)
}
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: Direction? = null,
search: String? = null,
filter: Filter = Filter()
): Paginated<WorkgroupEntity> {
return requester
.getFunction("find_workgroups")
.select(
page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search,
"filter" to filter
)
}
fun upsert(workgroup: WorkgroupSimple<CitizenRef>): WorkgroupEntity = requester
.getFunction("upsert_workgroup")
.selectOne("resource" to workgroup) ?: error("query 'upsert_workgroup' return null")
fun delete(workgroup: WorkgroupRef) = requester
.getFunction("delete_workgroup")
.perform("id" to workgroup.id)
fun addMember(workgroup: WorkgroupI, member: CitizenI): List<CitizenBasic> =
addMembers(workgroup, listOf(member))
fun addMembers(workgroup: WorkgroupI, members: List<CitizenI>): List<CitizenBasic> = requester
.getFunction("add_workgroup_members")
.select(
"id" to workgroup.id,
"resource" to members.serialize()
)
fun removeMember(workgroup: WorkgroupI, memberToDelete: CitizenI): List<CitizenBasic> =
removeMembers(workgroup, listOf(memberToDelete))
fun removeMembers(workgroup: WorkgroupI, membersToDelete: List<CitizenI>): List<CitizenBasic> = requester
.getFunction("remove_workgroup_members")
.select(
"id" to workgroup.id,
"resource" to membersToDelete
)
fun updateMembers(workgroup: WorkgroupI, members: List<CitizenI>): List<CitizenBasic> = requester
.getFunction("update_workgroup_members")
.select(
"id" to workgroup.id,
"resource" to members
)
fun <W : WorkgroupWithMembersI<CitizenI>> updateMembers(workgroup: W): W {
updateMembers(workgroup, workgroup.members).let {
workgroup.members = it
}
return workgroup
}
class Filter(
val createdById: String? = null
) : Parameter
}

View File

@@ -1,125 +0,0 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.event.ArticleUpdate
import fr.dcproject.event.raiseEvent
import fr.dcproject.repository.Article.Filter
import fr.dcproject.security.voter.ArticleVoter.Action.CREATE
import fr.dcproject.security.voter.ArticleVoter.Action.VIEW
import fr.dcproject.views.ArticleViewManager
import fr.ktorVoter.assertCan
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import kotlinx.coroutines.launch
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.repository.Article as ArticleRepository
@KtorExperimentalLocationsAPI
object ArticlesPaths {
@Location("/articles")
class ArticlesRequest(
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null,
val createdBy: 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
}
@Location("/articles/{article}")
class ArticleRequest(val article: ArticleEntity)
@Location("/articles/{article}/versions")
class ArticleVersionsRequest(
val article: ArticleEntity,
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
}
@Location("/articles")
class PostArticleRequest {
class Article(
val id: UUID?,
val title: String,
val anonymous: Boolean = true,
val content: String,
val description: String,
val tags: List<String> = emptyList(),
val draft: Boolean = false,
val versionId: UUID?
)
suspend fun getNewArticle(call: ApplicationCall): ArticleEntity = call.receive<Article>().run {
ArticleEntity(
id ?: UUID.randomUUID(),
title,
anonymous,
content,
description,
tags,
draft,
createdBy = call.citizen
).also {
it.versionId = versionId ?: UUID.randomUUID()
}
}
}
}
@KtorExperimentalLocationsAPI
fun Route.article(repo: ArticleRepository, viewManager: ArticleViewManager) {
get<ArticlesPaths.ArticlesRequest> {
val articles =
repo.find(it.page, it.limit, it.sort, it.direction, it.search, Filter(createdById = it.createdBy))
assertCan(VIEW, articles.result)
call.respond(articles)
}
get<ArticlesPaths.ArticleRequest> {
assertCan(VIEW, it.article)
it.article.views = viewManager.getViewsCount(it.article)
call.respond(it.article)
launch {
viewManager.addView(call.request.local.remoteHost, it.article, citizenOrNull)
}
}
get<ArticlesPaths.ArticleVersionsRequest> {
assertCan(VIEW, it.article)
repo.findVerionsByVersionsId(it.page, it.limit, it.article.versionId).let {
call.respond(it)
}
}
post<ArticlesPaths.PostArticleRequest> {
it.getNewArticle(call).also { article ->
assertCan(CREATE, article)
repo.upsert(article)
call.respond(article)
raiseEvent(ArticleUpdate.event, ArticleUpdate(article))
}
}
}

View File

@@ -1,83 +0,0 @@
package fr.dcproject.routes
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.JwtConfig
import fr.dcproject.entity.UserI.Roles.ROLE_USER
import fr.dcproject.messages.SsoManager
import fr.dcproject.routes.AuthPaths.LoginRequest
import fr.dcproject.routes.AuthPaths.RegisterRequest
import fr.dcproject.routes.AuthPaths.SsoRequest
import io.ktor.application.call
import io.ktor.auth.UserPasswordCredential
import io.ktor.features.BadRequestException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.User as UserRepository
@KtorExperimentalLocationsAPI
object AuthPaths {
@Location("/login")
class LoginRequest
@Location("/register")
class RegisterRequest
@Location("/sso")
class SsoRequest {
data class Content(val email: String, val url: String)
}
}
@KtorExperimentalLocationsAPI
@KtorExperimentalAPI
fun Route.auth(
userRepo: UserRepository,
citizenRepo: CitizenRepository,
ssoManager: SsoManager
) {
post<LoginRequest> {
try {
val credentials = call.receive<UserPasswordCredential>()
val user = userRepo.findByCredentials(credentials) ?: throw WrongLoginOrPassword()
call.respondText(JwtConfig.makeToken(user))
} catch (e: MismatchedInputException) {
call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request")
} catch (e: WrongLoginOrPassword) {
call.respond(HttpStatusCode.BadRequest, e.message)
}
}
post<RegisterRequest> {
try {
val citizen = call.receive<CitizenEntity>()
citizen.user.roles = listOf(ROLE_USER)
val created = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request")
call.respondText(JwtConfig.makeToken(created))
} catch (e: MissingKotlinParameterException) {
call.respond(HttpStatusCode.BadRequest)
}
}
post<SsoRequest> {
val content = call.receive<SsoRequest.Content>()
try {
ssoManager.sendEmail(content.email, content.url)
} catch (e: SsoManager.EmailNotFound) {
call.respond(HttpStatusCode.NotFound)
}
call.respond(HttpStatusCode.NoContent)
}
}
class WrongLoginOrPassword(override val message: String = "Username not exist or password is wrong") : Exception()

View File

@@ -1,94 +0,0 @@
package fr.dcproject.routes
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.citizen
import fr.dcproject.entity.Citizen
import fr.dcproject.routes.CitizenPaths.ChangePasswordCitizenRequest
import fr.dcproject.routes.CitizenPaths.CitizenRequest
import fr.dcproject.routes.CitizenPaths.CitizensRequest
import fr.dcproject.routes.CitizenPaths.CurrentCitizenRequest
import fr.dcproject.security.voter.CitizenVoter.Action.CHANGE_PASSWORD
import fr.dcproject.security.voter.CitizenVoter.Action.VIEW
import fr.ktorVoter.assertCan
import fr.postgresjson.repository.RepositoryI.Direction
import io.ktor.application.call
import io.ktor.auth.UserPasswordCredential
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.locations.put
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.User as UserRepository
@KtorExperimentalLocationsAPI
object CitizenPaths {
@Location("/citizens")
class CitizensRequest(
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: 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
}
@Location("/citizens/{citizen}")
class CitizenRequest(val citizen: Citizen)
@Location("/citizens/current")
class CurrentCitizenRequest
@Location("/citizens/{citizen}/password/change")
class ChangePasswordCitizenRequest(val citizen: Citizen) {
data class Content(val oldPassword: String, val newPassword: String)
}
}
@KtorExperimentalLocationsAPI
fun Route.citizen(
repo: CitizenRepository,
userRepository: UserRepository
) {
get<CitizensRequest> {
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
assertCan(VIEW, citizens.result)
call.respond(citizens)
}
get<CitizenRequest> {
assertCan(VIEW, it.citizen)
call.respond(it.citizen)
}
get<CurrentCitizenRequest> {
assertCan(VIEW, citizen)
call.respond(citizen)
}
put<ChangePasswordCitizenRequest> {
assertCan(CHANGE_PASSWORD, it.citizen)
try {
val content = call.receive<ChangePasswordCitizenRequest.Content>()
val currentUser = userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword))
val user = it.citizen.user
if (currentUser == null || currentUser.id != user.id) {
call.respond(HttpStatusCode.BadRequest, "Bad password")
} else {
user.plainPassword = content.newPassword
userRepository.changePassword(user)
call.respond(HttpStatusCode.Created)
}
} catch (e: MissingKotlinParameterException) {
call.respond(HttpStatusCode.BadRequest, "Request format is not correct")
}
}
}

View File

@@ -1,89 +0,0 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Comment
import fr.dcproject.entity.CommentRef
import fr.dcproject.routes.CommentPaths.CreateCommentRequest.Content
import fr.dcproject.security.voter.CommentVoter.Action.*
import fr.ktorVoter.assertCan
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.*
import io.ktor.request.receive
import io.ktor.request.receiveText
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
import java.util.*
import fr.dcproject.repository.CommentGeneric as CommentRepository
@KtorExperimentalLocationsAPI
object CommentPaths {
@Location("/comments/{comment}")
class CommentRequest(val comment: CommentRef)
@Location("/comments/{comment}/children")
class CommentChildrenRequest(
val comment: UUID,
page: Int = 1,
limit: Int = 50,
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
}
@Location("/comments/{comment}/children")
class CreateCommentRequest(val comment: CommentRef) {
class Content(val content: String)
}
}
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
fun Route.comment(repo: CommentRepository) {
get<CommentPaths.CommentRequest> {
val comment = repo.findById(it.comment.id)!!
assertCan(VIEW, comment)
call.respond(HttpStatusCode.OK, comment)
}
get<CommentPaths.CommentChildrenRequest> {
val comments =
repo.findByParent(
it.comment,
it.page,
it.limit
)
assertCan(VIEW, comments.result)
call.respond(HttpStatusCode.OK, comments)
}
post<CommentPaths.CreateCommentRequest> {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = Comment(
content = call.receive<Content>().content,
createdBy = citizen,
parent = parent
)
assertCan(CREATE, newComment)
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment)
}
put<CommentPaths.CommentRequest> {
val comment = repo.findById(it.comment.id)!!
assertCan(UPDATE, comment)
comment.content = call.receiveText()
repo.edit(comment)
call.respond(HttpStatusCode.OK, comment)
}
}

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