Merge pull request #77 from flecomte/refactoring-component-and-immutable

Big refactoring
This commit was merged in pull request #77.
This commit is contained in:
2021-03-24 19:06:06 +01:00
committed by GitHub
371 changed files with 13048 additions and 7668 deletions

3
.env
View File

@@ -1,10 +1,11 @@
NAME=dc-project APP_NAME=dc-project
DATABASE_URL=jdbc:postgresql:dc-project DATABASE_URL=jdbc:postgresql:dc-project
APP_PORT=8080 APP_PORT=8080
OPENAPI_PORT=8181 OPENAPI_PORT=8181
SONARQUBE_PORT=9002 SONARQUBE_PORT=9002
SONARQUBE_DB_PORT=5434
ELASTIC_REST=9200 ELASTIC_REST=9200
ELASTIC_NODES=9300 ELASTIC_NODES=9300

View File

@@ -1,34 +1,79 @@
<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="PACKAGES_TO_USE_STAR_IMPORTS"> <option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value> <value />
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option> </option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<PostgresCodeStyleSettings version="2"> <SqlCodeStyleSettings version="5">
<option name="myVersion" value="2" />
<option name="KEYWORD_CASE" value="1" /> <option name="KEYWORD_CASE" value="1" />
<option name="IDENTIFIER_CASE" value="1" /> <option name="IDENTIFIER_CASE" value="1" />
<option name="TYPE_CASE" value="1" /> <option name="TYPE_CASE" value="4" />
<option name="ALIAS_CASE" value="1" /> <option name="ALIAS_CASE" value="4" />
</PostgresCodeStyleSettings> <option name="BUILT_IN_CASE" value="4" />
<option name="QUOTE_IDENTIFIER" value="1" />
<option name="QUERY_EL_LINE" value="0" />
<option name="QUERY_IN_ONE_STRING" value="3" />
<option name="SUBQUERY_CONTENT" value="2" />
<option name="SUBQUERY_CLOSING" value="2" />
<option name="INSERT_OPENING" value="4" />
<option name="INSERT_CLOSING" value="0" />
<option name="INSERT_VALUES_EL_LINE" value="101" />
<option name="INSERT_COLLAPSE_MULTI_ROW_VALUES" value="true" />
<option name="SET_EL_LINE" value="1" />
<option name="SET_EL_WRAP" value="0" />
<option name="WITH_EL_LINE" value="101" />
<option name="WITH_EL_WRAP" value="2" />
<option name="SELECT_EL_LINE" value="101" />
<option name="SELECT_EL_COMMA" value="2" />
<option name="SELECT_NEW_LINE_AFTER_ALL_DISTINCT" value="true" />
<option name="SELECT_KEEP_N_ITEMS_IN_LINE" value="4" />
<option name="SELECT_USE_AS_WORD" value="1" />
<option name="FROM_EL_LINE" value="1" />
<option name="FROM_EL_COMMA" value="2" />
<option name="FROM_ALIGN_JOIN_TABLES" value="true" />
<option name="FROM_INDENT_JOIN" value="false" />
<option name="FROM_PLACE_ON" value="10" />
<option name="WHERE_EL_LINE" value="1" />
<option name="WHERE_EL_WRAP" value="3" />
<option name="ORDER_EL_LINE" value="101" />
<option name="ORDER_EL_WRAP" value="1" />
<option name="ORDER_EL_COMMA" value="2" />
</SqlCodeStyleSettings>
<codeStyleSettings language="SQL">
<option name="KEEP_LINE_BREAKS" value="false" />
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<indentOptions>
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="kotlin"> <codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="5" />
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings> </codeStyleSettings>
</code_scheme> </code_scheme>
</component> </component>

View File

@@ -2,4 +2,4 @@
<state> <state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" /> <option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state> </state>
</component> </component>

9
.idea/dataSources.xml generated
View File

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

5
.idea/gradle.xml generated
View File

@@ -4,8 +4,8 @@
<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="GRADLE" />
<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="modules"> <option name="modules">
@@ -13,7 +13,6 @@
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
</set> </set>
</option> </option>
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

3
.idea/misc.xml generated
View File

@@ -10,4 +10,7 @@
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="corretto-11" project-jdk-type="JavaSDK"> <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>
<component name="TaskProjectConfiguration">
<server type="GitHub" url="https://github.com" />
</component>
</project> </project>

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

@@ -0,0 +1,37 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="JUnit" factoryName="JUnit" singleton="false" show_console_on_std_err="true">
<useClassPathOnly />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="fr.dcproject.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="ALTERNATIVE_JRE_PATH" value="corretto-11" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="pattern" />
<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$" />
<patterns>
<pattern testClass="unit..*" />
<pattern testClass="functional..*" />
<pattern testClass="integration..*" />
</patterns>
<tag value="!functional" />
<method v="2">
<option name="Make" enabled="true" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="SQL Fixtures on DEV" run_configuration_type="ShConfigurationType" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Lint Format" run_configuration_type="GradleRunConfiguration" />
</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

@@ -2,6 +2,7 @@
<configuration default="false" name="Build and start all Docker" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker"> <configuration default="false" name="Build and start all 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="envFilePath" value="" />
<option name="envVars"> <option name="envVars">
<list> <list>
<DockerEnvVarImpl> <DockerEnvVarImpl>
@@ -11,6 +12,11 @@
</list> </list>
</option> </option>
<option name="commandLineOptions" value="--build" /> <option name="commandLineOptions" value="--build" />
<option name="secondarySourceFiles">
<list>
<option value="docker-compose-sonar.yml" />
</list>
</option>
<option name="services"> <option name="services">
<list> <list>
<option value="app" /> <option value="app" />
@@ -23,6 +29,8 @@
</list> </list>
</option> </option>
<option name="sourceFilePath" value="docker-compose.yml" /> <option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings> </settings>
</deployment> </deployment>
<method v="2" /> <method v="2" />

View File

@@ -1,9 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Check" type="CompoundRunConfigurationType">
<toRun name="Unit Tests (offline)" type="JUnit" />
<toRun name="Cucumber Tests (offline)" type="JUnit" />
<toRun name="Test All SQL" type="ShConfigurationType" />
<toRun name="Lint" type="GradleRunConfiguration" />
<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="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,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Cucumber Tests" type="JUnit" factoryName="JUnit">
<output_file path="$PROJECT_DIR$/var/log/test/cucumber.out.log" />
<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 -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" />
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<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>

23
.idea/runConfigurations/Down_Docker.xml generated Normal file
View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Down Docker" 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="testComposeDownForced" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

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,12 +1,12 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Unit Tests (offline)" type="JUnit" factoryName="JUnit" show_console_on_std_err="true"> <configuration default="false" name="Functional Tests" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" /> <output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly /> <useClassPathOnly />
<option name="PACKAGE_NAME" value="fr.dcproject" /> <option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" /> <option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="tags" /> <option name="TEST_OBJECT" value="tags" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.options=&quot;--tags ~@online&quot; -Djdk.attach.allowAttachSelf=true" /> <option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE"> <option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" /> <value defaultName="wholeProject" />
@@ -15,7 +15,7 @@
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" /> <env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs> </envs>
<dir value="$PROJECT_DIR$" /> <dir value="$PROJECT_DIR$" />
<tag value="!online" /> <tag value="functional" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

View File

@@ -1,13 +1,7 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Voter Tests" type="JUnit" factoryName="JUnit" show_console_on_std_err="true"> <configuration default="false" name="Functional 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" /> <output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly /> <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="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" /> <option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="" />
@@ -21,7 +15,7 @@
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" /> <env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs> </envs>
<dir value="$PROJECT_DIR$" /> <dir value="$PROJECT_DIR$" />
<tag value="voter" /> <tag value="functional&amp;!online" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

View File

@@ -1,13 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Opinion Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true"> <configuration default="false" name="Integration Tests" type="JUnit" factoryName="JUnit" singleton="false" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" /> <output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly /> <useClassPathOnly />
<option name="ALTERNATIVE_JRE_PATH" value="corretto-11" />
<option name="PACKAGE_NAME" value="" /> <option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" /> <option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" /> <option name="TEST_OBJECT" value="pattern" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@opinion&quot;" /> <option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE"> <option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" /> <value defaultName="wholeProject" />
@@ -16,7 +16,10 @@
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" /> <env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs> </envs>
<dir value="$PROJECT_DIR$" /> <dir value="$PROJECT_DIR$" />
<tag value="!online" /> <patterns>
<pattern testClass="integration..*" />
</patterns>
<tag value="!functional" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

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>

23
.idea/runConfigurations/Lint_Format.xml generated Normal file
View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Lint Format" type="GradleRunConfiguration" factoryName="Gradle" singleton="true">
<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="ktlintFormat" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<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" />
<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" />
</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>

23
.idea/runConfigurations/Migration.xml generated Normal file
View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Migration" 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="migration" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -6,8 +6,10 @@
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" /> <option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/src/main/resources/sql" /> <option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/src/main/resources/sql" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="false" /> <option name="INDEPENDENT_INTERPRETER_PATH" value="false" />
<option name="INTERPRETER_PATH" value="C:/Program Files/Git/bin/bash.exe" /> <option name="INTERPRETER_PATH" value="$PROJECT_DIR$/../../../../Program Files/Git/bin/bash.exe" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="false" />
<envs />
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>

View File

@@ -2,6 +2,7 @@
<configuration default="false" name="Run dependencies" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker"> <configuration default="false" name="Run dependencies" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml"> <deployment type="docker-compose.yml">
<settings> <settings>
<option name="envFilePath" value="" />
<option name="envVars"> <option name="envVars">
<list> <list>
<DockerEnvVarImpl> <DockerEnvVarImpl>
@@ -11,6 +12,11 @@
</list> </list>
</option> </option>
<option name="commandLineOptions" value="--build" /> <option name="commandLineOptions" value="--build" />
<option name="secondarySourceFiles">
<list>
<option value="docker-compose-sonar.yml" />
</list>
</option>
<option name="services"> <option name="services">
<list> <list>
<option value="db" /> <option value="db" />
@@ -18,9 +24,13 @@
<option value="rabbitmq" /> <option value="rabbitmq" />
<option value="redis" /> <option value="redis" />
<option value="openapi" /> <option value="openapi" />
<option value="sonarqube" />
<option value="sonarqube_db" />
</list> </list>
</option> </option>
<option name="sourceFilePath" value="docker-compose.yml" /> <option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings> </settings>
</deployment> </deployment>
<method v="2" /> <method v="2" />

View File

@@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run for dev" type="CompoundRunConfigurationType">
<toRun name="Run dependencies" type="docker-deploy" />
<toRun name="Run" type="GradleRunConfiguration" />
<method v="2" />
</configuration>
</component>

View File

@@ -6,8 +6,10 @@
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" /> <option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/src/main/resources/sql/fixtures/" /> <option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/src/main/resources/sql/fixtures/" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="false" /> <option name="INDEPENDENT_INTERPRETER_PATH" value="false" />
<option name="INTERPRETER_PATH" value="C:/Program Files/Git/bin/bash.exe" /> <option name="INTERPRETER_PATH" value="$PROJECT_DIR$/../../../../Program Files/Git/bin/bash.exe" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="false" />
<envs />
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>

View File

@@ -4,7 +4,7 @@
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-x test" /> <option name="scriptParameters" value="" />
<option name="taskDescriptions"> <option name="taskDescriptions">
<list /> <list />
</option> </option>
@@ -15,7 +15,9 @@
</option> </option>
<option name="vmOptions" value="" /> <option name="vmOptions" value="" />
</ExternalSystemSettings> </ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled> <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube 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="sonarqube" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</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,16 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start Elasticsearch" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="elasticsearch" />
</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>

28
.idea/runConfigurations/Test.xml generated Normal file
View File

@@ -0,0 +1,28 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" 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="test" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

23
.idea/runConfigurations/TestSql.xml generated Normal file
View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="TestSql" 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="testSql" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

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

View File

@@ -5,7 +5,7 @@
<option name="PACKAGE_NAME" value="fr.dcproject" /> <option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" /> <option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" /> <option name="TEST_OBJECT" value="tags" />
<option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" /> <option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE"> <option name="TEST_SEARCH_SCOPE">
@@ -15,6 +15,7 @@
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" /> <env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs> </envs>
<dir value="$PROJECT_DIR$" /> <dir value="$PROJECT_DIR$" />
<tag value="unit" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

View File

@@ -0,0 +1,30 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Unit, functional and integration tests" type="JUnit" factoryName="JUnit" singleton="false" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="ALTERNATIVE_JRE_PATH" value="corretto-11" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="pattern" />
<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$" />
<patterns>
<pattern testClass="unit..*" />
<pattern testClass="functional..*" />
<pattern testClass="integration..*" />
</patterns>
<tag value="!functional" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,13 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Comment Tests" type="JUnit" factoryName="JUnit" folderName="Cucumber" show_console_on_std_err="true"> <configuration default="false" name="Unit and functional tests" type="JUnit" factoryName="JUnit" singleton="false" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" /> <output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly /> <useClassPathOnly />
<option name="ALTERNATIVE_JRE_PATH" value="corretto-11" />
<option name="PACKAGE_NAME" value="" /> <option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" /> <option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" /> <option name="TEST_OBJECT" value="pattern" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.filter.tags=&quot;@comment&quot;" /> <option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE"> <option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" /> <value defaultName="wholeProject" />
@@ -16,7 +16,11 @@
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" /> <env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs> </envs>
<dir value="$PROJECT_DIR$" /> <dir value="$PROJECT_DIR$" />
<tag value="!online" /> <patterns>
<pattern testClass="unit..*" />
<pattern testClass="functional..*" />
</patterns>
<tag value="!functional" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

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

View File

@@ -15,12 +15,12 @@ help: ## This help.
bd: build-docker bd: build-docker
build-docker: ## Build the docker image of application build-docker: ## Build the docker image of application (alias: bd)
docker build -t dc-project -f docker/app/Dockerfile . docker build -t dc-project -f docker/app/Dockerfile .
pd: publish-docker pd: publish-docker
publish-docker: build-docker ## Publish docker image of application to Github publish-docker: build-docker ## Publish docker image of application to Github (alias: pd)
@git diff --quiet --exit-code || (echo "The git is DIRTY !!! You cannot publish this crap!" && exit 1) @git diff --quiet --exit-code || (echo "The git is DIRTY !!! You cannot publish this crap!" && exit 1)
@cat ./GH_TOKEN.txt | docker login docker.pkg.github.com -u ${GITHUB_USERNAME} --password-stdin @cat ./GH_TOKEN.txt | docker login docker.pkg.github.com -u ${GITHUB_USERNAME} --password-stdin
@docker tag dc-project docker.pkg.github.com/flecomte/dc-project/dc-project:${VERSION} @docker tag dc-project docker.pkg.github.com/flecomte/dc-project/dc-project:${VERSION}
@@ -28,27 +28,32 @@ publish-docker: build-docker ## Publish docker image of application to Github
rd: run-docker rd: run-docker
run-docker: ## Build and Run all docker services run-docker: ## Build and Run all docker services (alias: rd)
docker-compose up -d --build docker-compose up -d --build
rdd: run-docker-dependencies
run-docker-dependencies: ## Build and Run dependencies docker services (alias: rdd)
docker-compose up -d --build openapi rabbitmq redis elasticsearch db sonarqube_db sonarqube
pm: publish-maven pm: publish-maven
publish-maven: ## Publish JAR file to Github publish-maven: ## Publish JAR file to Github (alias: pm)
@git diff --quiet --exit-code || (echo "The git is DIRTY !!! You cannot publish this crap!" && exit 1) @git diff --quiet --exit-code || (echo "The git is DIRTY !!! You cannot publish this crap!" && exit 1)
gradlew publish gradlew publish
f: fixtures f: fixtures
fixtures: ## Import fixtures fixtures: ## Import fixtures (alias: f)
bash src/main/resources/sql/fixtures/fixtures.sh bash src/main/resources/sql/fixtures/fixtures.sh
reset-database: ## Import fixtures reset-database: ## Reset database !!!
cd src/main/resources/sql/ ; bash resetDB.sh cd src/main/resources/sql/ ; bash resetDB.sh
test-sql: ## Test sql test-sql: ## Test sql
cd src/test/sql/ ; bash test.sh 1 cd src/test/sql/ ; bash test.sh 1
v: vertion v: version
vertion: ## Show current version version: ## Show current version (alias: v)
@echo ${VERSION} @echo ${VERSION}

View File

@@ -1,15 +1,20 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import com.typesafe.config.ConfigFactory
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
import io.gitlab.arturbosch.detekt.Detekt
import org.gradle.internal.os.OperatingSystem
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.owasp.dependencycheck.reporting.ReportGenerator import org.owasp.dependencycheck.reporting.ReportGenerator
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
val ktor_version: String by project val ktorVersion = "1.5.0"
val kotlin_version: String by project val kotlinVersion = "1.4.30"
val coroutinesVersion: String by project val coroutinesVersion = "1.4.3"
val logback_version: String by project val logbackVersion = "1.2.3"
val koinVersion: String by project val koinVersion = "2.0.1"
val jackson_version: String by project val jacksonVersion = "2.12.1"
val cucumber_version: String by project
group = "com.github.flecomte" group = "com.github.flecomte"
version = versioning.info.run { version = versioning.info.run {
@@ -23,26 +28,133 @@ version = versioning.info.run {
plugins { plugins {
jacoco jacoco
application application
maven
id("maven-publish") id("maven-publish")
id("org.jetbrains.kotlin.jvm") version "1.3.50" kotlin("jvm") version "1.4.30"
kotlin("plugin.serialization") version "1.4.30"
id("com.github.johnrengelman.shadow") version "5.2.0" id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jlleitschuh.gradle.ktlint") version "8.2.0" id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
id("org.owasp.dependencycheck") version "5.1.0" id("org.owasp.dependencycheck") version "6.1.1"
id("org.sonarqube") version "2.7" id("org.sonarqube") version "3.1.1"
id("net.nemerosa.versioning") version "2.13.1" id("net.nemerosa.versioning") version "2.14.0"
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
id("com.avast.gradle.docker-compose") version "0.14.0"
} }
application { application {
mainClassName = "io.ktor.server.jetty.EngineMain" mainClassName = "io.ktor.server.jetty.EngineMain"
} }
buildscript {
repositories {
mavenLocal()
mavenCentral()
jcenter()
maven { url = uri("https://jitpack.io") }
}
dependencies {
classpath("com.typesafe:config:1.4.1")
classpath("com.github.flecomte:postgres-json:2.1.1")
}
}
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
} }
val migration by tasks.registering {
group = "application"
dependsOn(tasks.named("composeUp"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/../src/main/resources/application.conf")).resolve()
val connection = Connection(
host = config.getString("db.host"),
port = config.getInt("db.port"),
database = config.getString("db.database"),
username = config.getString("db.username"),
password = config.getString("db.password")
)
Migrations(
connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI()
).run {
run()
}
}
}
val migrationTest by tasks.registering {
group = "verification"
dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/../src/test/resources/application-test.conf")).resolve()
val connection = Connection(
host = config.getString("db.host"),
port = config.getInt("db.port"),
database = config.getString("db.database"),
username = config.getString("db.username"),
password = config.getString("db.password")
)
Migrations(
connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI()
).run {
run()
connection.disconnect()
}
}
}
val testSql by tasks.registering {
group = "verification"
dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/../src/test/resources/application-test.conf")).resolve()
val connection = Connection(
host = config.getString("db.host"),
port = config.getInt("db.port"),
database = config.getString("db.database"),
username = config.getString("db.username"),
password = config.getString("db.password")
)
Migrations(
connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI(),
file("$buildDir/../src/test/sql/fixtures").toURI()
).run {
run()
}
Requester.RequesterFactory(
connection = connection,
queriesDirectory = file("$buildDir/../src/test/sql").toURI()
).createRequester().run {
getQueries().map {
try {
it.sendQuery() == 0
} catch (e: Exception) {
false
}
}
}
connection.disconnect()
}
}
tasks.withType<Jar> { tasks.withType<Jar> {
manifest { manifest {
attributes( attributes(
@@ -53,23 +165,62 @@ tasks.withType<Jar> {
} }
} }
tasks { tasks.withType<KotlinCompile> {
named<ShadowJar>("shadowJar") { kotlinOptions {
mergeServiceFiles("META-INF/services") jvmTarget = "11"
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}") sourceCompatibility = "11"
targetCompatibility = "11"
} }
} }
val sourcesJar by tasks.creating(Jar::class) { tasks.named<ShadowJar>("shadowJar") {
mergeServiceFiles("META-INF/services")
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}")
}
tasks.sonarqube.configure { dependsOn(tasks.jacocoTestReport) }
val sourcesJar by tasks.registering(Jar::class) {
group = "build"
archiveClassifier.set("sources") archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource) from(sourceSets.getByName("main").allSource)
} }
tasks.test { tasks.test {
useJUnit() useJUnit()
useJUnitPlatform() useJUnitPlatform()
// maxHeapSize = "1G" systemProperty("junit.jupiter.execution.parallel.enabled", true)
dependsOn(testSql)
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
} }
apply(plugin = "docker-compose")
dockerCompose {
projectName = "dc-project"
useComposeFiles = listOf("docker-compose.yml")
startedServices = listOf("db", "elasticsearch", "rabbitmq", "redis")
stopContainers = false
removeVolumes = false
removeContainers = false
isRequiredBy(project.tasks.run)
createNested("test").apply {
projectName = "dc-project_test"
useComposeFiles = listOf("docker-compose-test.yml")
stopContainers = false
isRequiredBy(project.tasks.test)
isRequiredBy(project.tasks.named("testSql"))
}
createNested("sonarqube").apply {
projectName = "dc-project"
useComposeFiles = listOf("docker-compose-sonar.yml")
stopContainers = false
removeVolumes = false
removeContainers = false
// isRequiredBy(project.tasks.sonarqube)
}
}
tasks.sonarqube.configure { dependsOn(tasks.named("sonarqubeComposeUp")) }
publishing { publishing {
if (versioning.info.dirty == false) { if (versioning.info.dirty == false) {
repositories { repositories {
@@ -97,15 +248,53 @@ publishing {
} }
jacoco { jacoco {
toolVersion = "0.8.3" toolVersion = "0.8.6"
applyTo(tasks.run.get())
}
tasks.register<JacocoReport>("applicationCodeCoverageReport") {
executionData(tasks.run.get())
sourceSets(sourceSets.main.get())
} }
tasks.jacocoTestReport { tasks.jacocoTestReport {
dependsOn(tasks.test)
reports { reports {
xml.isEnabled = true xml.isEnabled = true
html.isEnabled = true
} }
} }
detekt {
buildUponDefaultConfig = true // preconfigure defaults
// config = files("$projectDir/config/detekt.yml") // point to your custom config defining rules to run, overwriting default behavior
// baseline = file("$projectDir/config/baseline.xml") // a way of suppressing issues before introducing detekt
reports {
html.enabled = true // observe findings in your browser with structure and code snippets
xml.enabled = true // checkstyle like format mainly for integrations like Jenkins
txt.enabled = true // similar to the console output, contains issue signature to manually edit baseline files
sarif.enabled = true // standardized SARIF format (https://sarifweb.azurewebsites.net/) to support integrations with Github Code Scanning
}
}
tasks.withType<Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
this.jvmTarget = "11"
}
val setMaxMapCount = tasks.create<Exec>("setMaxMapCount") {
group = "docker"
doFirst {
if (OperatingSystem.current().isWindows) {
commandLine("cmd", "/c", "Powershell -ExecutionPolicy Bypass; wsl -d docker-desktop sysctl -w vm.max_map_count=262144")
} else if (OperatingSystem.current().isLinux) {
commandLine("sysctl -w vm.max_map_count=262144")
}
}
}
tasks.named("testComposeUp").configure { dependsOn(setMaxMapCount) }
dependencyCheck { dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML) formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
} }
@@ -118,41 +307,46 @@ repositories {
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") implementation(gradleApi())
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutinesVersion")
implementation("io.ktor:ktor-server-jetty:$ktor_version") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
implementation("io.ktor:ktor-client-jetty:$ktor_version") implementation("io.ktor:ktor-server-jetty:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logback_version") implementation("io.ktor:ktor-client-jetty:$ktorVersion")
implementation("io.ktor:ktor-server-core:$ktor_version") implementation("ch.qos.logback:logback-classic:$logbackVersion")
implementation("io.ktor:ktor-locations:$ktor_version") implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-auth:$ktor_version") implementation("io.ktor:ktor-locations:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktor_version") implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-gson:$ktor_version") implementation("io.ktor:ktor-auth-jwt:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktor_version") implementation("io.ktor:ktor-websockets:$ktorVersion")
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:$ktorVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jackson_version") implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion")
implementation("net.pearx.kasechange:kasechange-jvm:1.1.0") implementation("net.pearx.kasechange:kasechange-jvm:1.3.0")
implementation("com.auth0:java-jwt:3.8.2") implementation("com.auth0:java-jwt:3.12.0")
implementation("com.github.jasync-sql:jasync-postgresql:1.0.7") implementation("com.github.jasync-sql:jasync-postgresql:1.1.6")
implementation("com.github.flecomte:postgres-json:1.2.1") implementation("com.github.flecomte:postgres-json:2.1.1")
implementation("com.github.flecomte:ktor-voter:1.0.1") implementation("com.sendgrid:sendgrid-java:4.7.1")
implementation("com.sendgrid:sendgrid-java:4.4.1") implementation("io.lettuce:lettuce-core:5.3.6.RELEASE") // TODO update to 6.0.2
implementation("io.lettuce:lettuce-core:5.2.2.RELEASE") implementation("com.rabbitmq:amqp-client:5.10.0")
implementation("com.rabbitmq:amqp-client:5.8.0")
implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1") implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
implementation("com.jayway.jsonpath:json-path:2.4.0") implementation("com.jayway.jsonpath:json-path:2.5.0")
implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.0")
testImplementation("io.ktor:ktor-server-tests:$ktor_version") testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock:$ktor_version") testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktor_version") testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion")
testImplementation("org.koin:koin-test:$koinVersion") testImplementation("org.koin:koin-test:$koinVersion")
testImplementation("io.mockk:mockk:1.9.3") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
testImplementation("org.junit.jupiter:junit-jupiter:5.5.0") testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.amshove.kluent:kluent:1.4") testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
testImplementation("io.cucumber:cucumber-java8:$cucumber_version") testImplementation("org.amshove.kluent:kluent:1.61")
testImplementation("io.cucumber:cucumber-junit:$cucumber_version") testImplementation("io.mockk:mockk-agent-api:1.10.6")
testImplementation("io.mockk:mockk-agent-jvm:1.10.6")
testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
testImplementation("com.thedeanda:lorem:2.1")
testImplementation("org.openapi4j:openapi-operation-validator:1.0.6")
testImplementation("org.openapi4j:openapi-parser:1.0.6")
} }

98
doc/schema/Article.puml Normal file
View File

@@ -0,0 +1,98 @@
@startuml
title Search / Get articles
actor Front
box Article API
control Controller
control Repository
entity Article
database Postgres
endbox
box View System
control ArticleViewManager
database Elasticsearch
endbox
box Notification System
control EventNotification
database RabbitMQ
database Redis
endbox
Front -> Controller++: GET /articles?page=1
Controller -> Repository++: find
Repository -> Postgres++: find_articles()
return
return
return: 200, Articles
newpage Create / Update Article
Front -> Controller: POST /article
activate Controller
Controller -> Controller: Convert dto to Entity
Controller -> Controller: Check Authorization
alt Authorize
Controller -> Repository++: upsert(entity)
Repository -> Postgres++: upsert_article
return
return
Controller -> Controller: Convert to dto
Front <-- Controller: 200, New Article
else not authorize
Front <-- Controller: 403, "Forbidden"
end
Controller -> EventNotification: raiseEvent(ArticleUpdate)
deactivate Controller
activate EventNotification
EventNotification ->> RabbitMQ
deactivate EventNotification
...
RabbitMQ -->> EventNotification++
EventNotification ->> : Send Email
EventNotification ->> Redis : Push Event Notification
return <<ACK>>
newpage get one article by id
Front -> Controller: GET /article/{article}
activate Controller
Controller -> Repository++: findById()
Repository -> Postgres++: find_article_by_id()
return
return
Controller -> Controller: Check Authorization
alt Authorize
Controller -> ArticleViewManager++: getViewsCount(Article)
ArticleViewManager -> Elasticsearch++
return
return
Controller -> Controller: Convert Article and Views to dto
Front <<-- Controller: 200, Article
else not authorize
Front <<-- Controller: 403, "Forbidden"
end
Controller -> ArticleViewManager++: increment the view counter
ArticleViewManager -> Elasticsearch++
return
return
deactivate Controller
newpage get article versions by id
Front -> Controller: GET /articles/{article}/versions
activate Controller
Controller -> Controller: Check Authorization
alt Authorize
Controller -> Repository++: findVersionsByVersionId
Repository -> Postgres++: find_articles_versions_by_version_id
return
return
Controller -> Controller: Convert to dto
Front <-- Controller: 200, Articles versions
else not authorize
Front <-- Controller: 403, "Forbidden"
end
deactivate Controller
@enduml

View File

@@ -0,0 +1,66 @@
@startuml
title Notification
|Server|
partition Event {
start
:Article is modified;
:Send message to "notification" exchange (RabbitMQ);
:RabbitMQ send message to "push" and "email" queue;
stop
}
split
partition Email {
-[hidden]->
:Consume "email" queue<
repeat :get next notification;
:Get followers of article from DB;
while (loop on followers)
:Send email to the citizen>
endwhile
:ACK>
repeat while()
detach
}
splitagain
partition Push {
-[hidden]->
:Consume "email" queue<
repeat :get next notification;
:Get followers of article from DB;
while (loop on followers)
:Send notification message to redis>
endwhile
:ACK>
repeat while()
detach
}
splitagain
partition "Notification direct" {
-[hidden]->
|Client|
start
:Client arrive on the web site;
:Connect to the websocket;
|Server|
:Get citizen notification
from redis;
while (on each notifications)
:Send notification to websocket>
endwhile(no notification left)
|Client|
:show notification;
|Server|
:Subscribe to redis event;
repeat :On new notification;
:Get new notification from redis;
:Send notification to websocket>
|Client|
:show notification;
|Server|
repeat while (wait notification)
detach
}
endsplit
@enduml

48
docker-compose-sonar.yml Normal file
View File

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

44
docker-compose-test.yml Normal file
View File

@@ -0,0 +1,44 @@
version: '3.8'
services:
rabbitmq:
container_name: ${APP_NAME}_rabbitmq_test
image: rabbitmq:management-alpine
ports:
- 5673:5672
- 15673:15672
redis:
container_name: ${APP_NAME}_redis_test
image: redis:6-alpine
ports:
- 6380:6379
elasticsearch:
container_name: ${APP_NAME}_elasticsearch_test
image: elasticsearch:6.7.1
ports:
- 9201:9200
- 9301:9300
healthcheck:
test: ["CMD", "curl", "-f", "http://elasticsearch:9200"]
interval: 3s
timeout: 2s
retries: 20
db:
container_name: ${APP_NAME}_postgresql_test
build:
context: docker/postgresql
ports:
- 15432:5432
environment:
POSTGRES_PASSWORD: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_PWD}
depends_on:
- elasticsearch
healthcheck:
test: [ "CMD", "pg_isready", "-q", "-d", "${DB_NAME}", "-U", "${DB_USER}" ]
interval: 3s
timeout: 2s
retries: 20

View File

@@ -1,15 +1,9 @@
# To execute this docker-compose yml file use docker-compose -f <file_name> up # To execute this docker-compose yml file use docker-compose -f <file_name> up
# 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.8'
services: services:
sonarqube:
container_name: sonarqube_${NAME}
image: sonarqube
ports:
- ${SONARQUBE_PORT}:9000
openapi: openapi:
container_name: openapi_${NAME} container_name: ${APP_NAME}_openapi
image: swaggerapi/swagger-ui image: swaggerapi/swagger-ui
ports: ports:
- ${OPENAPI_PORT}:8080 - ${OPENAPI_PORT}:8080
@@ -17,22 +11,22 @@ services:
URL: "http://localhost:8080" URL: "http://localhost:8080"
rabbitmq: rabbitmq:
container_name: rabbitmq_${NAME} container_name: ${APP_NAME}_rabbitmq
image: rabbitmq:management-alpine image: rabbitmq:management-alpine
ports: ports:
- ${RABBITMQ_PORT}:5672 - ${RABBITMQ_PORT}:5672
- ${RABBITMQ_MANAGEMENT_PORT}:15672 - ${RABBITMQ_MANAGEMENT_PORT}:15672
redis: redis:
container_name: redis_${NAME} container_name: ${APP_NAME}_redis
image: redis:6.0-rc-alpine image: redis:6-alpine
ports: ports:
- ${REDIS_PORT}:6379 - ${REDIS_PORT}:6379
volumes: volumes:
- redis-data:/var/lib/redis:rw - redis-data:/var/lib/redis:rw
app: app:
container_name: app_${NAME} container_name: ${APP_NAME}_app
build: build:
context: . context: .
dockerfile: docker/app/Dockerfile dockerfile: docker/app/Dockerfile
@@ -51,7 +45,7 @@ services:
- rabbitmq - rabbitmq
elasticsearch: elasticsearch:
container_name: elasticsearch_${NAME} container_name: ${APP_NAME}_elasticsearch
image: elasticsearch:6.7.1 image: elasticsearch:6.7.1
ports: ports:
- ${ELASTIC_REST}:9200 - ${ELASTIC_REST}:9200
@@ -63,7 +57,7 @@ services:
retries: 20 retries: 20
db: db:
container_name: postgresql_${NAME} container_name: ${APP_NAME}_postgresql
build: build:
context: docker/postgresql context: docker/postgresql
ports: ports:

View File

@@ -1,12 +1,13 @@
#### BUILD #### #### BUILD ####
FROM gradle:5.6.4-jdk11 AS build FROM gradle:6.8-jdk11 AS build
COPY --chown=gradle:gradle . /home/gradle/src COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src WORKDIR /home/gradle/src
RUN gradle build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck --no-daemon RUN gradle build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck --no-daemon
RUN gradle shadowJar
#### RUN #### #### RUN ####
FROM adoptopenjdk/openjdk11:jre-11.0.4_11-alpine FROM amazoncorretto:11-alpine as run
ENV APPLICATION_USER ktor ENV APPLICATION_USER ktor
RUN adduser -D -g '' $APPLICATION_USER RUN adduser -D -g '' $APPLICATION_USER

View File

@@ -1,10 +1,9 @@
ktor_version=1.2.2
kotlin.code.style=official kotlin.code.style=official
kotlin_version=1.3.40 systemProp.sonar.host.url=http://localhost:9002
coroutinesVersion=1.3.3 systemProp.sonar.login=admin
logback_version=1.2.1 systemProp.sonar.password=sonar
koinVersion=2.0.1 systemProp.sonar.projectKey=dc-project
jackson_version=2.9.9 systemProp.sonar.projectName=DC Project
cucumber_version=5.1.3 systemProp.sonar.java.coveragePlugin=jacoco
systemProp.sonar.host.url=http://localhost:9000 systemProp.sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
systemProp.sonar.login=1196e8015c20035f1aa91e881b95ce9d6e879c8a systemProp.sonar.kotlin.detekt.reportPaths=build/reports/detekt/detekt.xml

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-6.8-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

31
gradlew vendored
View File

@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath
@@ -154,19 +156,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else else
eval `echo args$i`="\"$arg\"" eval `echo args$i`="\"$arg\""
fi fi
i=$((i+1)) i=`expr $i + 1`
done done
case $i in case $i in
(0) set -- ;; 0) set -- ;;
(1) set -- "$args0" ;; 1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;; 2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;; 3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
@@ -175,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " " echo " "
} }
APP_ARGS=$(save "$@") APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules # Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

25
gradlew.bat vendored
View File

@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@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="-Xmx64m" "-Xms64m"
@@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -51,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -61,28 +64,14 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

@@ -1,314 +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.elasticsearch.configElasticIndexes
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.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) ?: 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 {
}
}
configElasticIndexes(get())
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,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,83 +0,0 @@
package fr.dcproject.elasticsearch
import org.elasticsearch.client.Request
import org.elasticsearch.client.RestClient
import org.slf4j.Logger
import org.slf4j.LoggerFactory
fun waitElasticsearchIsUp(client: RestClient) {
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.elasticsearch")
val request = Request("GET", "/_cluster/health")
repeat(40) {
runCatching {
client.performRequest(request).statusLine.statusCode
}.onSuccess {
if (it == 200) {
logger.debug("Elasticsearch is Ready! Continue...")
return
} else {
logger.debug("sleep 2s and retry...")
Thread.sleep(2000)
}
}.onFailure {
logger.debug("${it.message}, sleep 2s and retry...")
Thread.sleep(2000)
}
}
error("Elasticsearch is not ready")
}
fun configElasticIndexes(client: RestClient) {
waitElasticsearchIsUp(client)
/* Create index if not exist */
client.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)
}
}
}
}

View File

@@ -1,102 +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? = null,
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,
workgroup: WorkgroupSimple<CitizenRef>? = null
) : ArticleFull,
ArticleAuthI<CitizenBasicI>,
ArticleSimple(id, title, createdBy, draft, workgroup),
Viewable by ViewableImp() {
init {
tags = tags.distinct()
}
}
class ArticleForUpdate(
id: UUID?,
val title: String,
val anonymous: Boolean = true,
val content: String,
val description: String,
tags: List<String> = emptyList(),
val draft: Boolean = false,
val createdBy: CitizenRef,
val workgroup: WorkgroupRef? = null
) : ArticleRefVersioning(id) {
val tags: List<String> = tags.distinct()
}
open class ArticleSimple(
id: UUID? = null,
override var title: String,
override val createdBy: CitizenBasic,
override var draft: Boolean = false,
override var workgroup: WorkgroupSimple<CitizenRef>? = null
) : 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? = null,
versionNumber: Int? = null,
versionId: UUID = UUID.randomUUID()
) : ArticleRef(id),
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionNumber, versionId)
open class ArticleRef(
id: UUID? = null
) : ArticleI, TargetRef(id)
interface ArticleI : UuidEntityI, TargetI
interface ArticleSimpleI :
ArticleI,
EntityVersioning<UUID, Int>,
EntityCreatedBy<CitizenBasicI>,
EntityCreatedAt,
EntityDeletedAt,
Votable {
var title: String
var workgroup: WorkgroupSimple<CitizenRef>?
}
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,85 +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<WorkgroupAndRoles> = emptyList()
class WorkgroupAndRoles(
val roles: List<String>,
val workgroup: WorkgroupSimple<CitizenRef>
)
}
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,71 +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 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,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,95 +0,0 @@
package fr.dcproject.entity
import fr.dcproject.entity.WorkgroupWithMembersI.Member
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
import fr.postgresjson.entity.EntityI
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,
createdBy: CitizenBasic,
override var members: List<Member<CitizenBasic>> = emptyList()
) : WorkgroupWithAuthI<CitizenBasic>,
WorkgroupSimple<CitizenBasic>(
id,
name,
description,
logo,
anonymous,
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,
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
fun isMember(user: UserI): Boolean = members.isMember(user)
fun isMember(citizen: CitizenWithUserI): Boolean = members.isMember(citizen)
fun hasRole(expectedRole: Role, user: UserI): Boolean = members.hasRole(expectedRole, user)
fun hasRole(expectedRole: Role, citizen: CitizenI): Boolean = members.hasRole(expectedRole, citizen)
fun getRoles(user: UserI): List<Role> = members.getRoles(user)
fun getRoles(citizen: CitizenI): List<Role> = members.getRoles(citizen)
}
interface WorkgroupWithMembersI<Z : CitizenI> : WorkgroupI {
var members: List<Member<Z>>
class Member<C : CitizenI> (
val citizen: C,
val roles: List<Role> = emptyList()
) : EntityI {
enum class Role {
MASTER,
MANAGER,
EDITOR,
REPORTER
}
}
}
fun List<CitizenI>.hasCitizen(citizen: CitizenI): Boolean = this.map { it.id }.contains(citizen.id)
fun <Z : CitizenWithUserI> List<Member<Z>>.isMember(user: UserI): Boolean =
map { it.citizen.user.id }.contains(user.id)
fun <Z : CitizenI> List<Member<Z>>.isMember(citizen: CitizenI): Boolean =
map { it.citizen.id }.contains(citizen.id)
fun <Z : CitizenI> List<Member<Z>>.hasRole(expectedRole: Role, citizen: CitizenI): Boolean =
any { member -> member.citizen.id == citizen.id && member.roles.any { it == expectedRole } }
fun <Z : CitizenWithUserI> List<Member<Z>>.hasRole(expectedRole: Role, user: UserI): Boolean =
any { member -> member.citizen.user.id == user.id && member.roles.any { it == expectedRole } }
fun <Z : CitizenWithUserI> List<Member<Z>>.getRoles(user: UserI): List<Role> =
firstOrNull { it.citizen.user.id == user.id }?.roles ?: emptyList()
fun <Z : CitizenWithUserI> List<Member<Z>>.getRoles(citizen: CitizenI): List<Role> =
firstOrNull { it.citizen.id == citizen.id }?.roles ?: emptyList()
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,213 @@
package fr.dcproject.application
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.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.joda.JodaModule
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
import fr.dcproject.application.Env.PROD
import fr.dcproject.application.Env.TEST
import fr.dcproject.common.security.AccessDeniedException
import fr.dcproject.component.article.articleKoinModule
import fr.dcproject.component.article.routes.installArticleRoutes
import fr.dcproject.component.auth.ForbiddenException
import fr.dcproject.component.auth.authKoinModule
import fr.dcproject.component.auth.jwt.jwtInstallation
import fr.dcproject.component.auth.routes.installAuthRoutes
import fr.dcproject.component.auth.user
import fr.dcproject.component.citizen.citizenKoinModule
import fr.dcproject.component.citizen.routes.installCitizenRoutes
import fr.dcproject.component.comment.article.routes.installCommentArticleRoutes
import fr.dcproject.component.comment.commentKoinModule
import fr.dcproject.component.comment.constitution.routes.installCommentConstitutionRoutes
import fr.dcproject.component.comment.generic.routes.installCommentRoutes
import fr.dcproject.component.constitution.constitutionKoinModule
import fr.dcproject.component.constitution.routes.installConstitutionRoutes
import fr.dcproject.component.doc.routes.installDocRoutes
import fr.dcproject.component.follow.followKoinModule
import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes
import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes
import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.routes.installNotificationsRoutes
import fr.dcproject.component.opinion.opinionKoinModule
import fr.dcproject.component.opinion.routes.installOpinionRoutes
import fr.dcproject.component.views.viewKoinModule
import fr.dcproject.component.vote.routes.installVoteRoutes
import fr.dcproject.component.vote.voteKoinModule
import fr.dcproject.component.workgroup.routes.installWorkgroupRoutes
import fr.dcproject.component.workgroup.workgroupKoinModule
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.client.HttpClient
import io.ktor.client.engine.jetty.Jetty
import io.ktor.features.AutoHeadResponse
import io.ktor.features.CORS
import io.ktor.features.CallLogging
import io.ktor.features.ContentNegotiation
import io.ktor.features.DataConversion
import io.ktor.features.NotFoundException
import io.ktor.features.StatusPages
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.cio.websocket.pingPeriod
import io.ktor.http.cio.websocket.timeout
import io.ktor.jackson.jackson
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.server.jetty.EngineMain
import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog
import org.koin.dsl.module
import org.koin.ktor.ext.Koin
import org.koin.ktor.ext.get
import org.slf4j.event.Level
import java.time.Duration
import java.util.concurrent.CompletionException
fun main(args: Array<String>): Unit = EngineMain.main(args)
enum class Env { PROD, TEST }
@ExperimentalCoroutinesApi
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module(env: Env = PROD) {
install(Koin) {
Slf4jLog()
modules(
listOf(
if (env == TEST) module { single { Configuration("application-test.conf") } }
else module { single { Configuration() } },
KoinModule,
articleKoinModule,
authKoinModule,
citizenKoinModule,
commentKoinModule,
constitutionKoinModule,
followKoinModule,
opinionKoinModule,
viewKoinModule,
voteKoinModule,
workgroupKoinModule,
)
)
}
install(CallLogging) {
level = Level.INFO
}
install(DataConversion, converters)
install(Locations)
HttpClient(Jetty) {
engine {
}
}
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
}
get<NotificationConsumer>().run {
start()
environment.monitor.subscribe(ApplicationStopped) {
close()
}
}
install(Authentication, jwtInstallation(get()))
install(AutoHeadResponse)
install(ContentNegotiation) {
jackson {
propertyNamingStrategy = PropertyNamingStrategies.LOWER_CAMEL_CASE
registerModule(JodaModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
configure(SerializationFeature.INDENT_OUTPUT, true)
setDefaultPrettyPrinter(
DefaultPrettyPrinter().apply {
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
indentObjectsWith(DefaultIndenter(" ", "\n"))
}
)
}
}
install(Routing.Feature) {
// trace { application.log.trace(it.buildText()) }
installArticleRoutes()
installAuthRoutes()
installCitizenRoutes()
installCommentArticleRoutes()
installCommentRoutes()
installFollowArticleRoutes()
installFollowConstitutionRoutes()
installWorkgroupRoutes()
installOpinionRoutes()
installVoteRoutes()
installConstitutionRoutes()
installCommentConstitutionRoutes()
installNotificationsRoutes()
installDocRoutes()
}
install(StatusPages) {
exception<CompletionException> { e ->
val parent = e.cause?.cause
if (parent is GenericDatabaseException) {
call.respond(HttpStatusCode.BadRequest, parent.errorMessage.message!!)
} else {
throw e
}
}
exception<NotFoundException> { e ->
call.respond(HttpStatusCode.NotFound, e.message!!)
}
exception<AccessDeniedException> {
if (call.user == null) call.respond(HttpStatusCode.Unauthorized)
else call.respond(HttpStatusCode.Forbidden)
}
exception<ForbiddenException> {
call.respond(HttpStatusCode.Forbidden)
}
}
install(CORS) {
method(HttpMethod.Options)
method(HttpMethod.Put)
method(HttpMethod.Delete)
header(HttpHeaders.Authorization)
if (env == PROD) {
host("localhost:4200", schemes = listOf("http", "https"))
} else {
anyHost()
}
allowCredentials = true
allowSameOrigin = true
maxAgeInSeconds = Duration.ofDays(1).seconds
}
if (env == PROD) {
get<Migrations>().run()
}
}

View File

@@ -0,0 +1,46 @@
package fr.dcproject.application
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import java.net.URI
class Configuration(val config: Config) {
constructor(resourceBasename: String? = null) : this(if (resourceBasename == null) ConfigFactory.load() else ConfigFactory.load(resourceBasename))
interface Sql {
val migrationFiles: URI
val functionFiles: URI
val fixtureFiles: URI
}
val sql
get() = object : Sql {
override val migrationFiles: URI = this::class.java.getResource("/sql/migrations")?.toURI() ?: error("No migrations found")
override val functionFiles: URI = this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found")
override val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures")?.toURI() ?: error("No sql fixture found")
}
interface Database {
val host: String
val port: Int
var database: String
var username: String
var password: String
}
val database
get() = object : Database {
override val host: String = config.getString("db.host")
override val port: Int = config.getInt("db.port")
override var database: String = config.getString("db.database")
override var username: String = config.getString("db.username")
override var password: String = config.getString("db.password")
}
val envName: String = config.getString("app.envName")
val domain: String = config.getString("app.domain")
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")
}

View File

@@ -0,0 +1,31 @@
package fr.dcproject.application
import io.ktor.features.DataConversion
import io.ktor.util.KtorExperimentalAPI
import org.koin.core.context.GlobalContext
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import java.util.UUID
private typealias ConverterDeclaration = DataConversion.Configuration.() -> Unit
private inline fun <reified T> DataConversion.Configuration.get(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): T = GlobalContext.get().koin.rootScope.get(qualifier, parameters)
@KtorExperimentalAPI
val converters: ConverterDeclaration = {
convert<UUID> {
decode { values, _ ->
values.singleOrNull()?.let { UUID.fromString(it) }
}
encode { value ->
when (value) {
null -> listOf()
is UUID -> listOf(value.toString())
else -> throw InternalError("Cannot convert $value as UUID")
}
}
}
}

View File

@@ -0,0 +1,110 @@
package fr.dcproject.application
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
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.common.email.Mailer
import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.NotificationEmailSender
import fr.dcproject.component.notification.NotificationsPush
import fr.dcproject.component.notification.Publisher
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient
import org.koin.core.qualifier.named
import org.koin.dsl.module
@KtorExperimentalAPI
val KoinModule = module {
// SQL connection
single {
val config: Configuration = get()
Connection(
host = config.database.host,
port = config.database.port,
database = config.database.database,
username = config.database.username,
password = config.database.password
)
}
// Launch Database migration
single {
val config: Configuration = get()
Migrations(get(), config.sql.migrationFiles, config.sql.functionFiles)
}
// Redis client
single<RedisClient> {
val config: Configuration = get()
RedisClient.create(config.redis).apply {
connect().sync().configSet("notify-keyspace-events", "KEA")
}
}
single { NotificationsPush.Builder(get()) }
single {
val config: Configuration = get()
NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
}
// RabbitMQ
single<ConnectionFactory> {
val config: Configuration = get()
ConnectionFactory().apply { setUri(config.rabbitmq) }
}
// JsonSerializer
single<ObjectMapper> {
jacksonObjectMapper().apply {
registerModule(SimpleModule())
propertyNamingStrategy = PropertyNamingStrategies.LOWER_CAMEL_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 {
val config: Configuration = get()
Requester.RequesterFactory(
connection = get(),
functionsDirectory = config.sql.functionFiles
).createRequester()
}
// Mailer
single {
val config: Configuration = get()
Mailer(config.sendGridKey)
}
single {
val config: Configuration = get()
Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
}
single {
val config: Configuration = get()
NotificationEmailSender(get<Mailer>(), config.domain, get(), get())
}
}

View File

@@ -0,0 +1,11 @@
package fr.dcproject.common
interface BitMaskI {
val bit: Long
infix operator fun contains(which: BitMaskI): Boolean = bit and which.bit == which.bit
infix operator fun plus(mask: BitMaskI): BitMaskI = BitMask(mask.bit and this.bit)
infix operator fun minus(mask: BitMaskI): BitMaskI = BitMask(this.bit - mask.bit)
}
class BitMask(override val bit: Long) : BitMaskI

View File

@@ -1,4 +1,4 @@
package fr.dcproject.messages package fr.dcproject.common.email
import com.sendgrid.Method import com.sendgrid.Method
import com.sendgrid.Request import com.sendgrid.Request
@@ -24,4 +24,4 @@ class Mailer(
throw ex throw ex
} }
} }
} }

View File

@@ -0,0 +1,28 @@
package fr.dcproject.common.entity
import fr.dcproject.component.citizen.database.CitizenI
interface Created<C : CitizenI> : CreatedAt, CreatedBy<C> {
class Imp<C : CitizenI>(createdBy: C) :
Created<C>,
CreatedBy<C> by CreatedBy.Imp(createdBy),
CreatedAt by CreatedAt.Imp()
}
interface Updated<C : CitizenI> : UpdatedAt, UpdatedBy<C> {
class Imp<C : CitizenI>(updatedAt: C) :
Updated<C>,
UpdatedBy<C> by UpdatedBy.Imp(updatedAt),
UpdatedAt by UpdatedAt.Imp()
}
interface Deleted<C : CitizenI> : DeletedAt, DeletedBy<C> {
override fun isDeleted(): Boolean = (this as DeletedAt).isDeleted()
class Imp<C : CitizenI>(deletedAt: C) :
Deleted<C>,
DeletedBy<C> by DeletedBy.Imp(deletedAt),
DeletedAt by DeletedAt.Imp() {
override fun isDeleted(): Boolean = (this as Deleted<C>).isDeleted()
}
}

View File

@@ -0,0 +1,25 @@
package fr.dcproject.common.entity
import fr.dcproject.component.citizen.database.CitizenI
interface CreatedBy<T : CitizenI> {
val createdBy: T
class Imp<T : CitizenI>(override val createdBy: T) : CreatedBy<T>
}
interface UpdatedBy<T : CitizenI> {
val updatedBy: T
class Imp<T : CitizenI>(override val updatedBy: T) : UpdatedBy<T>
}
interface DeletedBy<T : CitizenI> {
val deletedBy: T?
fun isDeleted(): Boolean {
return deletedBy?.let { true } ?: false
}
class Imp<T : CitizenI>(override val deletedBy: T?) : DeletedBy<T>
}

View File

@@ -0,0 +1,30 @@
package fr.dcproject.common.entity
import org.joda.time.DateTime
/* Interface */
interface CreatedAt {
val createdAt: DateTime
class Imp(
override val createdAt: DateTime = DateTime.now()
) : CreatedAt
}
interface UpdatedAt {
val updatedAt: DateTime
class Imp(
override val updatedAt: DateTime = DateTime.now()
) : UpdatedAt
}
interface DeletedAt {
val deletedAt: DateTime?
fun isDeleted(): Boolean {
return deletedAt?.let {
it < DateTime.now()
} ?: false
}
class Imp(
override val deletedAt: DateTime? = null
) : DeletedAt
}

View File

@@ -0,0 +1,12 @@
package fr.dcproject.common.entity
import fr.postgresjson.entity.UuidEntityI
import java.util.UUID
interface EntityI : UuidEntityI {
override val id: UUID
}
open class Entity(id: UUID? = null) : EntityI {
override val id: UUID = id ?: UUID.randomUUID()
}

View File

@@ -1,21 +1,25 @@
package fr.dcproject.entity package fr.dcproject.common.entity
import fr.postgresjson.entity.immutable.EntityCreatedAt import fr.dcproject.component.article.database.ArticleRef
import fr.postgresjson.entity.immutable.EntityCreatedBy import fr.dcproject.component.citizen.database.CitizenI
import fr.postgresjson.entity.immutable.UuidEntity import fr.dcproject.component.comment.generic.database.CommentRef
import fr.postgresjson.entity.immutable.UuidEntityI import fr.dcproject.component.constitution.database.ConstitutionRef
import java.util.* import fr.dcproject.component.opinion.database.OpinionRef
import java.util.UUID
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
interface ExtraI<T : TargetI, C : CitizenI> : interface ExtraI<T : TargetI, C : CitizenI> :
UuidEntityI, EntityI,
EntityCreatedAt, HasTarget<T>,
EntityCreatedBy<C> { CreatedAt,
CreatedBy<C>
interface HasTarget<T : TargetI> {
val target: T val target: T
} }
open class TargetRef(id: UUID? = null, reference: String = "") : TargetI, UuidEntity(id) { open class TargetRef(id: UUID? = null, reference: String = "") : TargetI, Entity(id) {
final override val reference: String final override val reference: String
get() = if (field != "") field else TargetI.getReference(this) get() = if (field != "") field else TargetI.getReference(this)
@@ -25,7 +29,7 @@ open class TargetRef(id: UUID? = null, reference: String = "") : TargetI, UuidEn
} }
} }
interface TargetI : UuidEntityI { interface TargetI : EntityI {
enum class TargetName(val targetReference: String) { enum class TargetName(val targetReference: String) {
Article("article"), Article("article"),
Constitution("constitution"), Constitution("constitution"),
@@ -39,7 +43,7 @@ interface TargetI : UuidEntityI {
t.isSubclassOf(ArticleRef::class) -> TargetName.Article.targetReference t.isSubclassOf(ArticleRef::class) -> TargetName.Article.targetReference
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(Opinion::class) -> TargetName.Opinion.targetReference t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL") else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
} }
} }
@@ -55,4 +59,4 @@ interface TargetI : UuidEntityI {
} }
val reference: String val reference: String
} }

View File

@@ -0,0 +1,25 @@
package fr.dcproject.common.entity
import java.util.UUID
interface VersionableId {
val versionId: UUID
class Imp(
versionId: UUID? = null,
) : VersionableId {
override val versionId: UUID = versionId ?: UUID.randomUUID()
}
}
interface Versionable : VersionableId {
override val versionId: UUID
val versionNumber: Int
class Imp(
override val versionNumber: Int,
versionId: UUID? = null,
) : Versionable {
override val versionId: UUID = versionId ?: UUID.randomUUID()
}
}

View File

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

View File

@@ -0,0 +1,16 @@
package fr.dcproject.common.response
import fr.dcproject.common.entity.EntityI
import fr.postgresjson.connexion.Paginated
fun <E : EntityI> Paginated<E>.toOutput(setup: (E) -> Any): Any {
return object {
val count = this@toOutput.count
val currentPage = this@toOutput.count
val limit = this@toOutput.limit
val offset = this@toOutput.offset
val total = this@toOutput.total
val totalPages = this@toOutput.totalPages
val result = this@toOutput.result.map { setup(it) }
}
}

View File

@@ -0,0 +1,21 @@
package fr.dcproject.common.response
import fr.dcproject.component.citizen.database.CitizenCreatorI
import java.util.UUID
fun CitizenCreatorI.toOutput(): Any = this.let { c ->
object {
val id: UUID = c.id
val name: Any = c.name.let { n ->
object {
val firstName: String = n.firstName
val lastName: String = n.lastName
}
}
val user: Any = c.user.let { u ->
object {
val username: String = u.username
}
}
}
}

View File

@@ -0,0 +1,134 @@
package fr.dcproject.common.security
/** Responses of AccessControl */
enum class AccessDecision {
GRANTED,
DENIED;
/**
* Convert decision to boolean
*/
fun toBoolean(): Boolean = when (this) {
GRANTED -> true
DENIED -> false
}
}
abstract class AccessControl {
/**
* A Shortcut for return a GrantedResponse
*/
protected fun granted(message: String? = null, code: String? = null): GrantedResponse = GrantedResponse(this, message, code)
/**
* A Shortcut for return a DeniedResponse
*/
protected fun denied(message: String, code: String): DeniedResponse = DeniedResponse(this, message, code)
/**
* Check all responses and return DENIED if one is DENIED
*
* If the list of responses is empty, return GRANTED
*/
private fun AccessResponses.getOneResponse(): AccessResponse = this.firstOrNull { it.decision == AccessDecision.DENIED } ?: granted()
/**
* An helper to convert a list of subject into one response
*/
protected fun <S : List<T>, T> canAll(items: S, action: (T) -> AccessResponse): AccessResponse = items
.map { action(it) }
.getOneResponse()
}
/**
* Throw an Exception if AccessControl return a DENIED response
*/
fun <T : AccessControl> T.assert(action: T.() -> AccessResponse) {
action().assert()
}
/**
* Check all responses and return DENIED if one is DENIED
*
* If the list of responses is empty, return GRANTED
*/
fun AccessResponses.getOneResponse(): AccessResponse = this.firstOrNull { it.decision == AccessDecision.DENIED } ?: GrantedResponse(first().accessControl)
/**
* Throw an Exception if one response is DENIED
*/
fun AccessResponses.assert() = this.getOneResponse().assert()
class AccessDeniedException(private val accessResponses: AccessResponses) : Throwable(accessResponses.first().message) {
constructor(accessResponse: AccessResponse) : this(listOf(accessResponse))
/**
* Get first response
*/
fun first(): AccessResponse = accessResponses.first()
/**
* Check if the error code is present into the responses
*/
fun hasErrorCode(code: String): Boolean = accessResponses
.filter { it.decision == AccessDecision.DENIED }
.any { it.code == code }
/**
* Find and return the response than match with the error code
*/
fun getErrorCode(code: String): AccessResponse? = accessResponses
.firstOrNull { it.decision == AccessDecision.DENIED && it.code == code }
/**
* Get a list of messages of all responses
*/
fun getMessages(): List<String> = accessResponses
.mapNotNull { it.message }
/**
* Get the first message
*/
fun getFirstMessage(): String? = accessResponses
.first()
.message
}
/**
* The response that all AccessControl method return
* @see GrantedResponse
* @see DeniedResponse
*/
sealed class AccessResponse(
val decision: AccessDecision,
val accessControl: AccessControl,
val message: String?,
val code: String?
) {
/**
* Convert response as boolean
*/
fun toBoolean(): Boolean = decision.toBoolean()
/**
* Throw Exception if response if DENIED
*/
fun assert() {
if (this.decision == AccessDecision.DENIED) {
throw AccessDeniedException(this)
}
}
}
class GrantedResponse(
accessControl: AccessControl,
message: String? = null,
code: String? = null
) : AccessResponse(AccessDecision.GRANTED, accessControl, message, code)
class DeniedResponse(
accessControl: AccessControl,
message: String,
code: String
) : AccessResponse(AccessDecision.DENIED, accessControl, message, code)
typealias AccessResponses = List<AccessResponse>

View File

@@ -0,0 +1,6 @@
package fr.dcproject.common.utils
import org.joda.time.DateTime
import org.joda.time.format.ISODateTimeFormat
fun DateTime.toIso(): String = ISODateTimeFormat.dateTime().print(this)

View File

@@ -1,4 +1,4 @@
package fr.dcproject.utils package fr.dcproject.common.utils
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.PathNotFoundException import com.jayway.jsonpath.PathNotFoundException
@@ -26,4 +26,4 @@ fun String.getJsonField(jsonPath: String): Int? {
.warn("No value for Json path ${JsonPath.compile(jsonPath).path}") .warn("No value for Json path ${JsonPath.compile(jsonPath).path}")
null null
} }
} }

View File

@@ -1,4 +1,4 @@
package fr.dcproject.utils package fr.dcproject.common.utils
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -6,5 +6,5 @@ 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<*>): Logger = LoggerFactory.getLogger(thisRef.javaClass.packageName)
} }

View File

@@ -0,0 +1,27 @@
package fr.dcproject.common.utils
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import io.ktor.application.ApplicationCall
import io.ktor.application.log
import io.ktor.features.BadRequestException
import io.ktor.request.receive
import kotlin.reflect.typeOf
/**
* Receives content for this request.
* @param type instance of `KClass` specifying type to be received.
* @return instance of [T] received from this call, or `null` if content cannot be transformed to the requested type..
*/
@OptIn(ExperimentalStdlibApi::class)
public suspend inline fun <reified T : Any> ApplicationCall.receiveOrBadRequest(message: String = "Bad Request, wrong body request"): T {
return try {
receive<T>(typeOf<T>())
} catch (cause: MissingKotlinParameterException) {
application.log.debug("Conversion failed, throw bad exception", cause)
throw BadRequestException(message, cause)
} catch (cause: UnrecognizedPropertyException) {
application.log.debug("Conversion failed, throw bad exception", cause)
throw BadRequestException(message, cause)
}
}

View File

@@ -0,0 +1,15 @@
package fr.dcproject.common.utils
import java.net.URL
fun String.readResource(callback: (String) -> Unit = {}): String {
val content = callback::class.java.getResource(this)?.readText() ?: error("File not found")
callback(content)
return content
}
fun String.getResource(callback: (URL) -> Unit = {}): URL {
val content = callback::class.java.getResource(this) ?: error("File not found")
callback(content)
return content
}

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