461 Commits

Author SHA1 Message Date
1be608e6b2 Add Codacy badge 2021-03-25 02:05:05 +01:00
b13cd5544c add coveralls on CI 2021-03-25 01:53:57 +01:00
104f0fb3fc remove distZip & distTar 2021-03-24 23:06:38 +01:00
b2f40ff421 Restrict CI on pull_request on master 2021-03-24 21:56:58 +01:00
09e81620a1 rollback lintCheck after test, create task testAll 2021-03-24 21:32:28 +01:00
fe953fc967 lintCheck after test 2021-03-24 19:48:14 +01:00
453fd2225c Merge pull request #80
rename openapi file
2021-03-24 19:39:23 +01:00
70fd54d831 Fix destination installation doc 2021-03-24 19:35:11 +01:00
dcf7a2bc06 Rename openapi file 2021-03-24 19:34:39 +01:00
118af0170a Remove unused openapi file 2021-03-24 19:34:00 +01:00
0aa8089a9a Merge pull request #79
Add codefactor badge
2021-03-24 19:29:44 +01:00
fef5f3b396 CodeFactor badge 2021-03-24 19:27:09 +01:00
1838b90ac9 Merge pull request #78
Create CI
2021-03-24 19:12:41 +01:00
73fa2be91f Split CI task test 2021-03-24 19:11:04 +01:00
52183abd08 Lint 2021-03-24 19:08:41 +01:00
e19266d4cc Create CI Action 2021-03-24 19:07:02 +01:00
f458d7b674 Move SQL test files 2021-03-24 19:07:01 +01:00
29d4d6ec25 Merge pull request #77 from flecomte/refactoring-component-and-immutable
Big refactoring
2021-03-24 19:06:06 +01:00
03f68213e8 Test openapi schema of route /workgroup/*/members 2021-03-24 19:01:12 +01:00
66fa1ba840 Test openapi schema of route /workgroup/*
add test and fix update workgroup
2021-03-24 01:15:19 +01:00
89b2abc10e Test openapi schema of route /workgroup/* 2021-03-23 22:08:13 +01:00
b04408219d Test openapi schema of route /votes/* 2021-03-22 20:28:53 +01:00
5ef2345ea6 Test openapi schema of route /opinions/* 2021-03-22 02:57:33 +01:00
50b5ca03c6 Fix schema validator for parameters 2021-03-22 02:44:44 +01:00
9862e112eb Test openapi schema of GET /constitutions/{constitution}/follows 2021-03-22 00:52:23 +01:00
5dc1518cfc Test openapi schema of GET /citizens/{citizen}/follows/articles 2021-03-22 00:30:31 +01:00
fbd18e4c2e update wrapper 2021-03-21 22:36:52 +01:00
0e5943864d Test openapi schema of /follow/* 2021-03-20 02:43:53 +01:00
46cc61fc25 update kotlin 2021-03-20 02:00:05 +01:00
786b0bc949 create comment.toOutput() 2021-03-20 01:57:10 +01:00
8aa809c37e fix FindArticles schema 2021-03-20 01:25:23 +01:00
fdd69a687b create CitizenCreatorI.toOutput() 2021-03-20 01:20:58 +01:00
4c1ab796e4 Test openapi schema of /constitutions/* 2021-03-20 01:14:39 +01:00
c1a3590b2b Improve BitMaskI 2021-03-20 00:58:51 +01:00
c9ce2a9dc7 Refactor constitution entity 2021-03-20 00:55:39 +01:00
8701815288 Fix badRequest 2021-03-20 00:54:33 +01:00
d03b585372 Improve BitMaskI 2021-03-20 00:54:01 +01:00
c9879be72c create openapi schema ArticleListingResponse 2021-03-19 21:47:00 +01:00
ec8abf44b8 Idea config 2021-03-19 21:46:12 +01:00
776e0257fb add updatedAt for comment response 2021-03-19 21:19:53 +01:00
8f1f1f10d9 add required for paginated in openapi 2021-03-19 21:05:49 +01:00
cba3f1f4bc Change article.createdBy response 2021-03-19 21:04:55 +01:00
316b1e8318 Idea config 2021-03-18 21:09:55 +01:00
78a6cc417c Test openapi schema of
POST/GET /constitutions/{constitution}/comments
GET /citizens/{citizen}/comments/constitutions
2021-03-17 22:07:14 +01:00
564d612e9a Test openapi schema of GET /comments/{comment}/children 2021-03-17 20:55:58 +01:00
65bf9a75f0 Fix CommentConstitutionRepository repo 2021-03-17 18:37:39 +01:00
33ceb13cde Test openapi schema of GET /citizens/{citizen}/comments/articles 2021-03-17 18:30:46 +01:00
7765e6d27a Test openapi schema of PUT /comments/{comment} 2021-03-17 18:25:55 +01:00
ca402dbea7 Test openapi schema of /comments/{comment} 2021-03-17 18:09:05 +01:00
07ae50c40a Test openapi schema of /articles/{article}/comments 2021-03-17 17:48:42 +01:00
42dbd940a0 Test openapi schema of /articles/{article}/comments 2021-03-17 02:17:37 +01:00
0f768f3e4c Test openapi schema of /citizens/{citizen}/password/change 2021-03-17 00:46:05 +01:00
5fce37f269 Test openapi schema of /citizens/{id} 2021-03-17 00:28:01 +01:00
52bdcd0990 Test openapi schema of /citizens/current 2021-03-16 18:51:41 +01:00
1655f47044 Test openapi schema of /citizens 2021-03-16 18:23:40 +01:00
c07a57a591 Test openapi schema of Auth passwordless 2021-03-16 01:28:26 +01:00
0cf1aea9bf Test openapi schema of Register
Fix some routes
Improve Schema Validation
2021-03-16 00:53:10 +01:00
235de4e5ff Improve error on "no path found" in openapi file 2021-03-15 10:13:38 +01:00
869093ab25 Fix tests 2021-03-15 10:13:01 +01:00
189aa8d549 Test openapi schema response of Login 2021-03-15 02:02:26 +01:00
97b07fb424 Test openapi schema requestBody 2021-03-15 02:01:39 +01:00
9c88adbabd Test openapi schema response of FindArticlesVersion,GetOneArticle,UpsertArticle
change snack_case to camelCase
2021-03-12 23:32:32 +01:00
ed0873837b Test openapi schema parameters in path 2021-03-09 03:49:59 +01:00
6988365473 Test openapi schema parameters 2021-03-09 01:53:01 +01:00
4762275b5b Test openapi schema response of FindArticles 2021-03-08 23:13:48 +01:00
97c1e47db2 Fix openapi.yaml 2021-03-08 23:07:23 +01:00
bb637dd96a #71 Use response object for route FindArticles 2021-03-05 00:39:49 +01:00
9fc21f5459 clean warnings 2021-03-03 03:16:09 +01:00
d9deb4836e #72 Move Entity and repository on the same package 2021-03-03 02:46:51 +01:00
79feed54dd #68 Clean vote and citizen Entity
remove last warnings
2021-03-03 02:25:24 +01:00
2e8215eafc Add command to change vm.max_map_count before run docker 2021-03-03 01:25:23 +01:00
4c00095118 #68 Clean workgroup Entity 2021-03-02 23:25:00 +01:00
bc772f168f #68 Clean opinion Entity 2021-03-02 00:53:02 +01:00
8d93fc8b3c #68 Clean follow Entity 2021-03-01 22:44:17 +01:00
66dcff8f46 Fix sonar task 2021-02-28 23:31:11 +01:00
c61b31cc58 Change codestyle 2021-02-28 02:07:12 +01:00
78604301c3 Remove pitest 2021-02-28 02:05:19 +01:00
d382a89905 Fix tests 2021-02-28 01:42:11 +01:00
7446bd506a create gradle tasks "test", "testSql", "migrations" and run docker before tasks 2021-02-27 23:20:57 +01:00
c25cf64f4b #68 Clean Entities 2021-02-25 18:32:17 +01:00
c1af949204 fix sonarqube 2021-02-25 01:33:20 +01:00
7985ea67e5 remove cucumber 2021-02-25 01:09:44 +01:00
9a3a308841 Change code style 2021-02-25 00:53:20 +01:00
d83ba2d54d Add Integration test for workgroup routes 2021-02-25 00:48:52 +01:00
7b4066b928 Add Integration test for vote routes 2021-02-24 00:21:29 +01:00
9fb2262107 Add Integration test for opinion routes 2021-02-23 22:51:16 +01:00
a27099177d Add SQL request find_citizen_by_name 2021-02-23 22:06:05 +01:00
b0aec9bea0 Add Integration test for follow constitution routes 2021-02-23 17:24:12 +01:00
a17bd11e9e Add Integration test for follow article routes 2021-02-19 23:30:04 +01:00
8cf79a791e Rename annonymous to anonymous 2021-02-19 22:27:47 +01:00
bf4e01e318 Add Integration test for constitution routes 2021-02-19 22:27:47 +01:00
d29bb4467a Add/fix tags to integration tests 2021-02-19 21:58:06 +01:00
0ecf0c205f Add Integration test for comment 2021-02-19 00:42:10 +01:00
55aa512aa5 Change error text 2021-02-13 01:43:47 +01:00
addb3cddff refactor idea run's 2021-02-13 01:34:05 +01:00
f7b6cc4eb3 fix DB test script 2021-02-13 01:28:05 +01:00
28b9ac4e54 Install pitest 2021-02-13 01:27:40 +01:00
02879291e8 Sonarqube and detekt 2021-02-11 01:50:14 +01:00
066b01e86f Move all file in fr.dcproject. 2021-02-11 01:50:14 +01:00
c85401aa86 move createCitizen of Integration test 2021-02-10 18:10:21 +01:00
34a7310944 move Integration step test 2021-02-10 17:48:32 +01:00
e8716a1e7f Add Integration test for article 2021-02-10 14:37:57 +01:00
f8ecd69582 Rename tests 2021-02-10 01:24:34 +01:00
99438b1ff9 Fix engine start/stop for integration tests 2021-02-10 01:08:23 +01:00
ec0115d613 Add Integration test for citizen 2021-02-10 00:58:42 +01:00
55bfbb619d Add Integration test for Register 2021-02-09 20:56:27 +01:00
edf0c00cf1 Init Integration test without cucumber 2021-02-09 03:11:43 +01:00
dcf35eaccd Clean Citizen entities
Change plainPassword to just password
Add request Input for /login
2021-02-09 00:39:26 +01:00
905330a722 Move last package into common/component 2021-02-06 01:25:59 +01:00
5979337bc3 Remove last converter for Workgroup 2021-02-06 01:06:46 +01:00
8c228f666f Lint 2021-02-06 00:56:02 +01:00
678bdf7087 Remove converter for OpinionChoice 2021-02-06 00:45:54 +01:00
507698c7ea use always receiveOrBadRequest 2021-02-06 00:32:07 +01:00
fdd4742b28 Remove converter for Citizen
Add receiveOrBadRequest
2021-02-06 00:23:36 +01:00
0bbe37c6d5 Remove converter for CitizenRef 2021-02-05 23:13:37 +01:00
192411a69a Remove converter for Constitution 2021-02-05 22:18:32 +01:00
929d248841 Remove converter for ConstitutionRef 2021-02-05 21:52:10 +01:00
8ead83941f Remove converter for CommentRef 2021-02-05 21:48:33 +01:00
aeaab860b2 Remove converter for article 2021-02-05 00:32:19 +01:00
16eadc0921 Optimise GetOneArticle 2021-02-04 23:58:53 +01:00
f2445f3b25 Optimise ArticleVersionsRequest 2021-02-04 23:53:17 +01:00
bb212f9c6c move notification to component 2021-02-04 23:34:20 +01:00
89c15eb1cf cleanup and refactoring of notification
close rabbit and redis connexion on application close
Refactoring of Configuration class
fix notification id increment
Add builder for NotificationPush
Add close to notificationPush to remove listener
Clean tags of tests
purge queue before functional tests
2021-02-04 02:37:29 +01:00
a05b5edc86 update redis to stable version 2021-02-03 15:35:34 +01:00
5704eb9e07 Clean config in test 2021-02-03 15:35:14 +01:00
3580c33891 Rename NotificationConsumer 2021-02-03 01:49:12 +01:00
3b3a71f6eb Schema for notifications 2021-02-03 01:22:20 +01:00
d479cf6bca Refactor Notification System
Add Tests for notification system
2021-02-03 01:21:13 +01:00
b54a40cef4 Rename event to notification 2021-01-27 01:08:09 +01:00
1c644768e6 remove raiseEvent for notifications
Add Test for EventNotification
Add application.conf for test
2021-01-26 23:58:25 +01:00
aa95de7a6a move some part of KoinModule in components 2021-01-23 23:26:01 +01:00
dd6433306d Remove Configuration object to koin 2021-01-23 22:47:02 +01:00
bfc0b7e796 Move comment constitution to component 2021-01-23 22:38:47 +01:00
81e14f1a84 Move some interface to common package 2021-01-23 22:28:48 +01:00
d9f19a9c23 reword 2021-01-23 22:20:44 +01:00
1e5598cb91 Move constitution to component 2021-01-23 21:18:42 +01:00
f34407962b Remove Deprecated Article Entities 2021-01-23 00:54:53 +01:00
49a03a57cb Rename Voter to AccessControl 2021-01-22 22:07:25 +01:00
c1b8b508ac Move vote to component 2021-01-22 21:45:02 +01:00
c92d0b5640 Move opinions to component 2021-01-22 21:11:04 +01:00
73e96c0c46 Use interface PaginatedRequestI 2021-01-22 20:37:47 +01:00
fac27d0725 move database config env in object 2021-01-22 20:25:32 +01:00
97ccb6ee51 ktlint 2021-01-22 17:29:36 +01:00
93aa47c6cc move routes installation into component bis 2021-01-21 22:01:10 +01:00
667339979b move routes installation into component 2021-01-21 21:55:24 +01:00
3ba4a195ba Update gradle and dependencycheck 2021-01-21 16:26:15 +01:00
6a32895571 Move Follow to a component 2021-01-18 21:45:48 +01:00
6cdc526335 upgrade klint and format code (remove wildcard import 2021-01-18 17:23:16 +01:00
a79e1ec086 upgrade kotlin, ktor, sendgrid 2021-01-18 17:13:24 +01:00
4b435b925e upgrade java-jwt, kasechange-jvm, amqp-client, lettuce 2021-01-18 13:30:25 +01:00
78e01ba981 upgrade logback, corouties, json-path, mockk, junit-jupiter 2021-01-18 13:24:51 +01:00
56818189ae upgrade jackson 2021-01-18 13:17:11 +01:00
fbea05218b upgrade cucumber 2021-01-18 13:15:12 +01:00
c78dfc0f7f upgrade jasync-sql 2021-01-18 13:12:47 +01:00
425d01c0df Remove ktor-voter 2021-01-18 13:12:29 +01:00
64fa0912b8 Refactoring of OpinionChoiceVoter 2021-01-18 13:03:01 +01:00
ba673943d8 Refactoring of OpinionVoter 2021-01-18 09:51:48 +01:00
c196bfadbc Fix Move Views config into component 2021-01-18 01:08:11 +01:00
e9f56412c5 Move Views config into component 2021-01-18 01:07:02 +01:00
d12c9c2166 Move installation of JWT to external file in auth component 2021-01-18 00:09:23 +01:00
55cd97078a Refactoring of FollowVoter 2021-01-17 23:46:51 +01:00
d6840e8064 Refactoring of VoteVoter 2021-01-17 23:32:43 +01:00
308a284280 Refactoring of ConstitutionVoter 2021-01-17 23:06:18 +01:00
1b6549eae3 Rename wrong naming SSO to Passwordless 2021-01-17 22:46:43 +01:00
b028ff05b9 Move files
Move Application and configurations file to the application package
Move JWT files to the auth.jwt package
Move ApplicationContext to auth package an rename to CitizenContext
2021-01-17 22:29:32 +01:00
c380ba47a5 Refactoring of WorkgroupVoter 2021-01-17 15:01:49 +01:00
ecda29abe5 Move Workgroup to a component 2021-01-17 14:18:19 +01:00
bec73561e7 Fix IDEA commands 2021-01-17 14:17:53 +01:00
299495a03c ktlint 2021-01-17 00:10:51 +01:00
d87b433398 Move Comment article to a Component 2021-01-17 00:05:37 +01:00
b61fc3c7d1 Fix Auth request 2021-01-17 00:04:48 +01:00
b421b03575 Fix Double token generation in SSO 2021-01-15 23:43:49 +01:00
128510fe88 Add TODO's 2021-01-15 23:43:49 +01:00
ce90884758 Move Auth to a Component 2021-01-15 23:27:47 +01:00
42440c0041 Fix sql-test launcher 2021-01-15 02:46:10 +01:00
459397f8e7 Move tests and create a command to run all tests 2021-01-15 02:40:41 +01:00
7c106f7cf8 Refactoring of CommentVoter 2021-01-15 01:45:32 +01:00
caadc2a969 klint 2021-01-14 22:53:48 +01:00
91ab800272 Move comments classes into comment component 2021-01-14 22:51:33 +01:00
64f74d0449 fix namespace of article refactoring 2021-01-14 22:15:51 +01:00
6a8c5bf717 Refactors Citizen into component
Refactor CitizenVoter
Split citizens routes
2021-01-14 15:24:05 +01:00
a1c1accc87 Refactors Articles and Voter
- Move files into components (article)
- Split articles routes
- Refactoring for remove ktor-voter (ArticleVoter)
- Remove mutability
- Move DataConversion to separate file (Converter.kt)
- Add Schemas for Articles routes
- Fix SQL Query for Workgroup roles
- rename container_name in docker-compose
2021-01-14 13:06:13 +01:00
03401f711e Update ktor-voter to version 2.2.0 2020-10-05 15:32:44 +02:00
74923891d0 Fix security 2020-10-04 01:10:22 +02:00
317e029f79 remove "restart: always" on docker compose 2020-10-01 15:52:08 +02:00
46ee98c97f Increase article fixtures 2020-10-01 15:51:36 +02:00
269bf09c66 update installation doc 2020-10-01 15:17:40 +02:00
9d6ad08834 Update idea config 2020-09-29 09:51:00 +02:00
06b1296192 Add commande to reset database in Makefile 2020-09-04 15:22:36 +02:00
5def84282d Improve vote count with cache 2020-09-04 15:22:15 +02:00
24d8f1d58b Fix route /citizens/current if not logged 2020-09-04 11:10:22 +02:00
a3b44588a9 Can filter workgroup by members #58
add filterNotNull to toUUID() function
2020-08-25 15:30:36 +02:00
16feab9655 #57 I can filter articles by workgroup
update lib "postgres-json:1.2.1"
2020-08-24 16:55:18 +02:00
01c3d85a17 #54 Can create article under the name of the Workgroup 2020-07-10 15:41:39 +02:00
3d319605de Fix find_workgroups SQL query 2020-06-14 00:32:39 +02:00
63c4568aeb Fix find_citizens SQL query 2020-06-14 00:30:45 +02:00
e2d1b697b7 add missing parameters in openApi definition for "GET /workgroups" route 2020-06-14 00:30:24 +02:00
70092efaf5 Improve README 2020-06-02 16:31:38 +02:00
9c10a88a36 Improve Makefile 2020-06-02 15:20:11 +02:00
2ee8b80596 Add descriptions on OpenAPI 2020-06-02 15:08:12 +02:00
ee60f5b4e7 Fix tests 2020-06-02 15:08:05 +02:00
10928251e6 Can update roles of one citizen on workgroup 2020-06-02 14:20:35 +02:00
9b79301662 update workgroup in OpenApi 2020-06-02 01:01:29 +02:00
32510652d1 Fix upsert_workgroup 2020-06-02 01:01:08 +02:00
a6c36c542e Fixture not delete table content before apply 2020-06-01 13:52:48 +02:00
7874f5cec4 #55 Can be assign a role to members of my Workgroup
Remove Owner on Workgroup (use role MASTER instead)
"find_citizen_by_id" not return user anymore, use "find_citizen_by_id_with_user" instead
2020-06-01 13:46:15 +02:00
8ff6fcc970 Add volume to docker for redis 2020-05-27 09:56:54 +02:00
bd123d03e9 Fix fixtures script 2020-05-14 13:46:55 +02:00
ac852baaf6 Improve README 2020-05-14 13:46:44 +02:00
159b9de19a Create a auto build docker image
Version of application is calculated by the git tags
2020-05-14 00:54:21 +02:00
e4a85722f1 Move Elasticsearch configuration into external function "configElasticIndexes" 2020-05-13 13:58:17 +02:00
eca5d1fe33 #39 I must be notified by email when an article is changed
add function CitizenI.Name.getFullName()
2020-05-12 16:15:35 +02:00
5db451ef0e Fix cucumber tests 2020-05-12 12:04:18 +02:00
5008b8b69f Add repository Opinion.addOpinion() 2020-05-12 12:03:52 +02:00
4504600268 Remove sub directories 2020-05-12 10:36:16 +02:00
678a2f48d2 #48 Can view workgroup of citizen 2020-05-12 01:26:22 +02:00
460222bf69 Create bash script to insert fixtures 2020-05-12 01:25:01 +02:00
b497c61cfc Create bash script to execute SQL tests 2020-05-12 00:59:16 +02:00
36d60ce6a3 Fix openapi filename 2020-05-11 02:29:20 +02:00
0253480b10 Improve SQL test script 2020-05-11 02:29:02 +02:00
ecae3848ea Add script to launch fixtures 2020-05-11 01:51:30 +02:00
8640d9146d Update postgres-json to Add ShadowJar compatibility
Add .env for redis and ES
change group name of project
2020-05-11 01:50:40 +02:00
b4be28ddc8 fix sql tests command 2020-04-15 09:22:42 +02:00
8d5ee0c130 Fix postgresql setup files 2020-04-15 01:21:54 +02:00
e16c1eedc3 remove .idea/vcs file 2020-04-15 00:57:59 +02:00
7dc1772708 Move Entity.Request to Routes 2020-04-15 00:43:59 +02:00
9e50e3af58 update dependencies 2020-03-25 16:42:12 +01:00
e572ca0024 #24 Move voter code to an external library 2020-03-25 02:08:31 +01:00
575752cdc7 Add workgroups routes to openapi 2020-03-25 00:24:50 +01:00
07315a5624 Add route put/delete 2020-03-25 00:23:40 +01:00
8c25f7633e Add delete workgroup query & repo 2020-03-24 23:48:11 +01:00
e85c8a3d55 find_workgroup_by_id return members
fix find_workgroup_members if no members
2020-03-24 23:38:07 +01:00
75af31ab73 Fix following in other version of article 2020-03-22 03:44:26 +01:00
589b6f5245 Refactoring of updateOpinions (route/repo/query)
Can nw be set multiple opinion on sigle query
fix OpinionVoter on CREATE
2020-03-22 00:53:08 +01:00
479793503c fix find_citizen_opinions_by_target_id if json is empty 2020-03-22 00:43:09 +01:00
ddea05aea0 Change 404 to 200 if no comment/vote/follow on article 2020-03-20 02:11:56 +01:00
c55eba4219 Fix Mark as read notification 2020-03-20 01:33:48 +01:00
64e3fb0134 Add script to launch SQL test 2020-03-18 02:18:10 +01:00
06684120ce Add transaction for each steps 2020-03-18 00:52:37 +01:00
a99eaf3eef Fix comment voter 2020-03-18 00:51:42 +01:00
118193210b Merge branch 'refactoring-test' 2020-03-17 15:52:53 +01:00
cf2881c890 #42 Improve VoteVoter 2020-03-17 15:49:59 +01:00
4b96080051 #42 Improve CommentVoter 2020-03-17 15:14:44 +01:00
890c84c762 #42 Add tests for WorkgroupVoter 2020-03-17 15:01:13 +01:00
c0becd8fbd #42 Add tests for VoteVoter 2020-03-17 14:21:38 +01:00
622deb0f7d #42 Add tests for OpinionVoter 2020-03-17 14:08:14 +01:00
86b569123c #42 Add tests for OpinionChoiceVoter 2020-03-17 12:13:14 +01:00
1055e14039 #42 Add Tag to voter tests 2020-03-17 12:01:02 +01:00
45dbdecdb2 #42 Improve tests for ArticleVoter 2020-03-17 11:57:49 +01:00
d8251f1dd2 #42 Add tests for FollowVoter 2020-03-17 11:47:25 +01:00
559adb7e2a #42 Add tests for CommentVoter 2020-03-17 11:20:29 +01:00
60d887c5cc #42 Add tests for CitizenVoter 2020-03-17 02:20:32 +01:00
bc7bfc3fef Fix tests 2020-03-17 01:47:35 +01:00
f225f7345c Lint 2020-03-17 01:34:30 +01:00
676baa4e28 Refactoring Workgroup Tests 2020-03-17 01:26:44 +01:00
0932da452b Refactoring Vote Tests 2020-03-17 01:11:04 +01:00
8c7e5bdf9b Refactoring Opinion Tests
upsert_opinion_choice
2020-03-17 00:31:24 +01:00
6b4a6f4075 Refactoring Constitution Tests 2020-03-16 22:35:23 +01:00
0e912ef4b1 Refactoring Follows Tests
Add missing route GET /constitutions/{constitution}/follows
2020-03-16 21:24:24 +01:00
10b2844620 Refactoring CommentArticle Tests 2020-03-16 20:39:16 +01:00
bbe54c3115 Refactoring CommentArticle Tests 2020-03-16 20:27:20 +01:00
74503c9d3a Refactoring Comment Tests 2020-03-16 15:02:43 +01:00
561905f7ab Refactoring Citizen Tests 2020-03-16 15:01:50 +01:00
68238494e5 Refactoring Auth Tests 2020-03-16 13:35:12 +01:00
0221d7023a Refactoring Article Tests 2020-03-16 13:30:24 +01:00
ca78db4155 #42 Add tests to ArticleVoter
Refactor ArticleVoter
2020-03-16 03:47:22 +01:00
aa7ca26b51 Improve voter 2020-03-16 03:47:22 +01:00
8ad0281003 #29 Implement Workgroup members routes (Add, remove, update) 2020-03-15 20:15:39 +01:00
f277613820 #29 Implement Workgroup members query 2020-03-13 23:33:51 +01:00
373ce8e593 Create Test for extension readResource 2020-03-13 23:33:51 +01:00
27232c5ca9 #29 Implement Workgroup (route, voter, repo, entity)
Create tests for workgroup routes
add CitizenWithUserI
2020-03-13 23:33:50 +01:00
dc034f7c51 Improve KtorServerRequestSteps 2020-03-13 21:00:01 +01:00
491ca13284 #29 Implement Workgroup (create query)
Create SQL query to get/set workgroup
Add workgroup to article query
2020-03-13 20:56:58 +01:00
fb3278fa47 migration of IDE files 2020-03-11 12:56:48 +01:00
90e7e0254d #15 Count total view
Create Viewable interface
Set Article as Viewable
Create ES client
Now view article increment view count
Create index on start application

add an extention in ApplicationCall, to get citizen or return null
2020-03-11 12:55:39 +01:00
f677cac779 Lint 2020-03-06 09:58:08 +01:00
559890000d Refactoring: user fixtures in sql tests 2020-03-06 09:54:58 +01:00
4a4e9651fd Move Notifications config from Application.kt to ConfigNotification.kt 2020-03-03 12:23:42 +01:00
0421f3cb55 Can delete Notification
Add ID to notification
2020-02-28 23:43:51 +01:00
f3e0f64249 Improve Notification WS 2020-02-28 16:01:10 +01:00
d34ae52522 Fix unfollow on multiple version of article/constitution 2020-02-27 22:56:41 +01:00
e8d342a729 Fix CORS for delete request 2020-02-27 22:55:52 +01:00
977d59b60b Add Test for get follow of article 2020-02-27 20:34:46 +01:00
5d26497cdb Minor refactoring of cucumber steps 2020-02-27 20:34:29 +01:00
cd13f9a010 Improve Follow repository to split IN/OUT type 2020-02-27 20:31:25 +01:00
3bf20a971a Fix find_citizen_votes_by_target_ids
lint
2020-02-27 19:09:25 +01:00
967b370a41 implement route /articles/{article}/follows
create repository for find_follow
2020-02-27 17:14:56 +01:00
3a08052728 Create find_follow.sql 2020-02-27 17:12:32 +01:00
1418dd95bc Implement Websocket for push Notification
create auth with jwt in query string
2020-02-27 01:38:34 +01:00
b678f7f2cc Add consumer for Notification 2020-02-24 20:44:37 +01:00
af33ed9ec3 Publish message into rabbitmq on create article
Create Redis and Rabbit in docker-compose
2020-02-22 02:26:52 +01:00
471013984c Continue to implement opinion
improve target reference
Improve Tests for Opinion
fix SQL:upsert_opinion
2020-02-19 12:33:29 +01:00
60bd24e653 Add sonarqube (Docker server & gradle config) 2020-02-12 17:16:52 +01:00
4a2d18ff87 Create route for Opinions
create OpinionRepository
create OpinionVoter
create OpinionChoiceRef
create extention String.toUUID() and List<String>.toUUID()
create OpinionAggregation
create interface RequestBuilderWithCreator for create entity by request
rename opinion_list to opinion_choice
create sql function find_citizen_opinions
fix sql function find_citizen_opinions_by_target_id
fix sql funciton find_opinion_choices
2020-02-12 14:47:18 +01:00
ec6e39b130 Create OpinionChoice 2020-02-11 23:03:12 +01:00
42781565dd fix find_citizen_opinions_by_target_ids.sql
change opinion_list.target to array
2020-02-08 02:28:19 +01:00
976f8fac6a Add find_opinions.sql 2020-02-08 01:24:16 +01:00
7742287884 Add Opinion entity and add opinions on article 2020-02-08 01:03:16 +01:00
f622dbeecd Add opinion on find_article_by_id & find_articles request
create request for find_citizen_opinions_by_target_id
create fixture for opinions
2020-02-08 01:01:32 +01:00
2048c71881 Fix sql test and add Command for lanch all sql test 2020-02-07 02:09:09 +01:00
de6ca63165 Add opinion Tables and request 2020-02-07 02:06:06 +01:00
a4dbd43cfb Fix get comments by target 2020-02-06 01:45:21 +01:00
eb7a0c7210 Add sort on comments -> findByTarget 2020-02-05 15:20:38 +01:00
42a41da066 Add OpenAPI route and server 2020-02-04 16:45:54 +01:00
77658c5f6b Fix get comments children 2020-01-30 17:30:11 +01:00
5161dca1d5 Fix get all commet of article 2020-01-30 15:02:52 +01:00
24bc1520f7 Improve change password 2020-01-30 14:18:40 +01:00
3d2d3c2e14 Remove Extra class, and create CommentRef 2020-01-29 16:58:14 +01:00
41a98f23b8 Clean code 2020-01-28 22:35:50 +01:00
813d6857e9 Split Entities for remove nullable variables 2020-01-28 22:00:44 +01:00
3cdd1f3a46 update postgresjson (mutable entity) 2020-01-22 23:08:58 +01:00
e68b5d87ce remove wrong line in sql file 2020-01-22 01:04:14 +01:00
ec99df1f6a update config 2020-01-22 00:55:22 +01:00
a3b35b6273 add more config 2020-01-22 00:38:33 +01:00
e97ac8368f catch WrongLoginOrPassword 2020-01-22 00:38:33 +01:00
db744e5f31 respond 404 on email not found on sso connect 2020-01-22 00:38:31 +01:00
6746ca08e2 update config 2020-01-22 00:03:35 +01:00
5b4d878f8a Can filter article by author 2019-10-28 23:02:16 +01:00
846b23b550 Can sort by vote and popularity 2019-10-16 16:36:03 +02:00
32417d3276 VoteAggregation return total votes and the score 2019-10-16 16:27:49 +02:00
54393000b3 Fix comment::children_count 2019-10-16 14:40:21 +02:00
8cb5c35296 Can Comment a Comment 2019-10-15 15:46:19 +02:00
499fbd6dcf add 401 to OpenAPI 2019-10-15 15:00:13 +02:00
11b0fad0ed define missin route "/citizens/{citizen}/comments/articles" into OpenAPI 2019-10-15 13:30:14 +02:00
e73a7b5a18 Add App into docker 2019-10-13 16:24:13 +02:00
f76f6f83bb Lint project 2019-10-10 22:50:24 +02:00
194620e15b add Lint and test report 2019-10-10 22:50:03 +02:00
95f6087b8f update gradle 2019-10-10 22:49:52 +02:00
2d1d2d532a Fix link into SSO email 2019-10-10 00:01:00 +02:00
f5030933ef add openApi for sso and change password 2019-10-09 23:40:27 +02:00
7e52808ccf Clean code after update postgresjson 2019-10-09 22:54:34 +02:00
a6f25bcbb2 Can login with SSO & change Password 2019-10-09 21:57:56 +02:00
20416ce108 create SQL function "change_user_password" 2019-10-09 12:27:20 +02:00
f91b25b35b add email to citizen entity 2019-10-08 14:37:21 +02:00
ebc552a431 add email to citizen table 2019-10-08 14:36:45 +02:00
afb7f7a1a6 Implement Mailer 2019-10-08 02:31:23 +02:00
c156e2a7b1 SQL functions find_comments_* now return VotesAggregates
create Votable Interface
remove usless "resourceTarget" argument for SQL function "count_vote"
2019-10-08 00:26:41 +02:00
f20964878f Improve Vote Comment
Add "targetReference" field into Extra Entity
Add VoteCommentRoute to OpenApi
2019-10-07 14:15:43 +02:00
d90d3117d5 Can vote on Comment 2019-10-06 23:50:42 +02:00
646c199292 Improve error message on sql function 2019-10-06 23:50:27 +02:00
6d4339f2a5 Refactor SQL function Comment 2019-10-06 23:49:20 +02:00
9cbba66a36 Can search article by tags 2019-10-06 00:01:28 +02:00
5b44d4766d OpenApi: Add tags 2019-10-05 22:17:39 +02:00
f7feb513e0 OpenApi: move comment path parameter to generic type 2019-10-05 22:17:39 +02:00
bc8d187914 OpenApi: move constitution path parameter to generic type 2019-10-05 22:17:39 +02:00
e55e0aea44 OpenApi: move article path parameter to generic type 2019-10-05 22:17:38 +02:00
c5a2f92b19 create SQL function for get citizen votes with multiple target ids
add updated_at field on vote table
2019-10-05 22:17:38 +02:00
51cc5b640d fix and improve cucumber step "findJsonElement" 2019-10-03 23:01:59 +02:00
fad625f204 Fix: get All vote of current citizen now return complete target 2019-10-03 15:43:31 +02:00
fc138790ea Can get current Citizen 2019-10-02 01:28:43 +02:00
9de2c91191 Add openApi for /citizens/{citizen}/votes/articles 2019-10-01 17:13:11 +02:00
5352c82bba Can get votes for one citizen 2019-10-01 13:58:25 +02:00
5d1bed5f22 Improve test "the response should contain object" 2019-10-01 13:57:05 +02:00
70c69bd626 Fix comment test 2019-10-01 13:55:41 +02:00
47bdc349c5 create SQL function find_votes_by_citizen 2019-10-01 11:30:07 +02:00
be03bc4df8 GET comments of article is paginate 2019-10-01 09:54:18 +02:00
3a77eff86e can get comment children of an other comment 2019-09-22 23:54:53 +02:00
dc490dcac3 change parent_id in comment 2019-09-22 22:39:04 +02:00
a37afc1ada return votes after vote 2019-09-20 22:42:50 +02:00
2311d3986e add PUT in CORS 2019-09-20 02:37:16 +02:00
68acd3b075 fix migration with function upsert_article 2019-09-18 01:07:37 +02:00
93e32caa4c fix draft on SQL tests 2019-09-18 01:07:03 +02:00
acc6ecf114 add votes to article 2019-09-18 01:06:40 +02:00
a5b55c2d87 add draft to constitution 2019-09-16 22:55:09 +02:00
bb555ce69f docker elastic always restart 2019-09-16 22:50:17 +02:00
c2beed416e replace Article entity by Article request for the HTTP request
Add draft
2019-09-16 22:47:37 +02:00
05c28a2f62 find only the last version of article on listing 2019-09-06 02:20:35 +02:00
56ea1507f7 Add draft for article & constitution, auto set last_version column 2019-09-06 00:36:41 +02:00
d7d88a3295 fix sql test 2019-09-06 00:35:48 +02:00
742927a590 Add route for get all versions of one article 2019-09-05 17:45:24 +02:00
6423491be3 Add pagination on query find_articles_versions_by_* 2019-09-05 17:14:34 +02:00
4990015769 fix openApi 2019-09-05 16:37:26 +02:00
3533eb4a5c Add query for get all versions of one article 2019-09-05 16:37:15 +02:00
369967d5f3 add versions on articles fixtures 2019-09-05 16:36:48 +02:00
7151da1f3f deduplicate tags 2019-09-05 11:23:42 +02:00
9cf05865a0 fix citizen voter 2019-09-05 11:21:30 +02:00
3a4a90abfe fix openAPI 2019-09-05 11:21:01 +02:00
70b11cec7c Add CORS 2019-09-05 11:19:33 +02:00
1b6b748b01 increment created_at column on fixtures for article and constitution 2019-09-01 01:33:40 +02:00
cbec0e134b feature #26: use ZDB index to find article and constitution 2019-09-01 01:32:18 +02:00
befa456a38 feature #26: Add ZDB index to constitution, article and comment 2019-09-01 01:31:29 +02:00
cb91c50e58 Add security for follow 2019-08-31 00:14:05 +02:00
52dfaaf814 Add Pagination Object on OpenApi 2019-08-30 22:33:34 +02:00
9e88b33595 improve security. 2019-08-30 22:32:30 +02:00
f5bff403f0 feature #8: create Vote Constitution route 2019-08-30 15:51:55 +02:00
96362bbb66 update idea config 2019-08-30 15:50:59 +02:00
5e5405e5f9 feature #8: Add definition in openApi for Vote Article 2019-08-30 15:40:17 +02:00
5d78f8d4c6 Refactor Voter 2019-08-30 15:15:29 +02:00
d1999d84ca feature #8: Add security for Vote Article 2019-08-30 14:34:24 +02:00
3e21884b38 add deleted_at on article and constitution 2019-08-30 14:26:28 +02:00
45a8f42335 improve cucumber implementation 2019-08-30 14:25:57 +02:00
bc75f5d9e2 refactor Voter 2019-08-30 14:01:53 +02:00
16b71fb1e6 rename annonymous to anonymous 2019-08-30 13:25:54 +02:00
1b8a02c2b3 feature #8: create Vote Article route 2019-08-30 13:24:37 +02:00
7a8f8d3d6a add IDEA config for local connexion "test" 2019-08-30 12:09:26 +02:00
e861c29b69 feature #11: describe "create/delete follow to constitution" in openAPI 2019-08-30 10:12:57 +02:00
fffecd5072 feature #11: describe "create/delete follow to article" in openAPI 2019-08-30 10:12:57 +02:00
2f079d9d9d feature #11: describe "Extra" component in openAPI 2019-08-30 10:10:03 +02:00
9c3dce7511 feature #11: describe "get/edit comment to constitution" in openAPI 2019-08-30 10:10:03 +02:00
31f1504e56 feature #11: describe "get/edit comment to article" in openAPI 2019-08-30 10:10:03 +02:00
639b839058 feature #11: describe "get/edit comment" in openAPI 2019-08-29 22:58:49 +02:00
00dbcaf7ab feature #11: describe "post article" in openAPI
add security
2019-08-29 22:36:41 +02:00
cb7a2c2eaf Add articles to title entity 2019-08-29 22:28:13 +02:00
569f8a5fc3 feature #11: describe "constitutions routes" in openAPI 2019-08-29 22:27:26 +02:00
fb2b393c2e feature #11: add schemas CreatedAt in openAPI 2019-08-29 14:42:24 +02:00
201430017e remove fixed raw citizen and use real current citizen
Fix tests
2019-08-29 14:38:11 +02:00
025153c4cd force createdBy on upsert constitution 2019-08-29 14:37:00 +02:00
83961fd202 force createdBy on upsert article 2019-08-29 14:25:00 +02:00
299304f379 feature #11: describe "get article(s)" in openAPI 2019-08-29 14:23:14 +02:00
20e9c3f7ef Refactor openApi parameters 2019-08-29 14:11:08 +02:00
679dd2e4c5 Auth require for view citizens 2019-08-29 13:44:46 +02:00
9fb6440736 feature #11: describe "get citizens" in openAPI 2019-08-29 13:42:22 +02:00
11fae5f19c feature #11: describe "get citizen" in openAPI 2019-08-29 13:30:08 +02:00
c057a4aad3 feature #11: describe login in openAPI 2019-08-29 10:30:57 +02:00
bff45ab9cf feature #11: describe register in openAPI 2019-08-29 02:22:50 +02:00
8e94d97c2b force ROLE_USER on register 2019-08-29 02:21:49 +02:00
b00456b31a Application not run migration if test 2019-08-28 23:38:28 +02:00
4c2da6ab71 Implement comment constitution
fix bugs
2019-08-28 23:32:43 +02:00
33f4992b5e fix "CommentRepository.findByCitizen" 2019-08-28 17:12:33 +02:00
51ea82d2a4 add query "find_reference_by_id" 2019-08-28 17:10:50 +02:00
b5cb238061 feature #7: check if comment content is realy edited
add somes tests
2019-08-27 23:18:42 +02:00
1fb0e39038 feature #7: Add routes for comment article 2019-08-27 22:31:01 +02:00
ff1e34c616 Add cucumbers steps for article 2019-08-27 22:31:01 +02:00
b2230f2fed feature #7: add find_comment_by_id query 2019-08-27 22:30:56 +02:00
ff76bd55ef update postgresjson 2019-08-27 22:30:22 +02:00
67665350eb Create query for find citizen By Username 2019-08-27 22:29:14 +02:00
2c717609b1 Clean code 2019-08-27 22:29:14 +02:00
bfcbfee120 add citizen in ApplicationCall 2019-08-27 22:29:14 +02:00
adba5a5aad Refactor cucumber steps 2019-08-27 22:29:14 +02:00
0e50921a0a Move paths to separated files 2019-08-27 22:29:14 +02:00
b285bad593 feature #7: add function "find_comments_by_parent" 2019-08-27 22:29:11 +02:00
d6fda26fbd feature #7: add function "find_comments_by_target" 2019-08-27 22:29:08 +02:00
0547cf5784 feature #7: add colum parents_ids into comment 2019-08-27 22:29:05 +02:00
ce38aa6fe2 feature #7: add query "find_comments_article_by_citizen" and "find_comments_constitution_by_citizen" 2019-08-27 22:28:58 +02:00
f167cf02f9 feature #7: add query "find_comments_by_citizen" 2019-08-27 22:26:39 +02:00
d911109cd2 Add test for bad request 2019-08-25 01:19:35 +02:00
53e8bc024c Add some sql check 2019-08-25 00:11:37 +02:00
f887b99536 feature #9: Create route for register 2019-08-24 02:03:29 +02:00
29463e310e Add SQL query for insert citizen with user 2019-08-24 00:28:25 +02:00
1adbef817a Move SQL tests 2019-08-23 17:09:45 +02:00
d5499576ed Fix SQL tests 2019-08-23 17:08:19 +02:00
36be09eed2 hide log of jasync lib 2019-08-23 16:47:14 +02:00
4f5cd827c4 Add Security to Citizen 2019-08-23 16:45:33 +02:00
9b6f3aab88 Refactoring of cucumber implementation 2019-08-23 13:04:20 +02:00
46885ac599 Add insertUser into repo 2019-08-23 12:19:09 +02:00
0108d496e0 feature #9: Create Voter for Article 2019-08-23 09:41:41 +02:00
21b6a525fd feature #9: Add security for route "create article" 2019-08-23 00:03:53 +02:00
5542eede27 feature #9: Add routes for login 2019-08-22 23:23:25 +02:00
4e9f737e00 feature #14: Add routes for get follows of one citizen 2019-08-22 09:47:34 +02:00
9b2b5e681f feature #14: Add routes for follow/unfollow coonstitution 2019-08-22 09:47:34 +02:00
7ae30bd3cd feature #23: create route for citizen 2019-08-22 09:47:34 +02:00
9821833dd8 feature #14: create route for unfollow article 2019-08-22 09:47:34 +02:00
17746d9b1e improve speed of cucumber tests 2019-08-22 09:47:33 +02:00
7dffc005b9 feature #14: create route for follow article 2019-08-22 09:47:33 +02:00
c1f228e3c5 fix unfollow function 2019-08-22 09:47:33 +02:00
d03b8dcebb add logs 2019-08-22 09:47:33 +02:00
f060943565 feature #12: add Constitution tests 2019-08-22 09:47:33 +02:00
41b1f75cfb Add RepositityTest 2019-08-22 09:47:32 +02:00
86d699c9c0 feature #12: Add constitution Entity, repository and route 2019-08-22 09:47:16 +02:00
6131935036 feature: #6: improve tests 2019-08-22 09:47:16 +02:00
6ffe529b4e update for postgresjson 2019-08-22 09:46:44 +02:00
376 changed files with 17691 additions and 1448 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
build
out
GH_TOKEN.txt
Makefile
var
gradle
.idea
.gradle
docker
gradlew
gradlew.bat
docker-compose.yml
src/test

20
.env
View File

@@ -1,13 +1,29 @@
NAME=dc-project
APP_NAME=dc-project
DATABASE_URL=jdbc:postgresql:dc-project
APP_PORT=8080
OPENAPI_PORT=8181
SONARQUBE_PORT=9002
SONARQUBE_DB_PORT=5434
ELASTIC_REST=9200
ELASTIC_NODES=9300
ELASTICSEARCH_CONNECTION=http://elasticsearch:9200
POSTGRESQL_PORT=5432
DB_HOST=db
DB_PORT=5432
DB_NAME=dc-project
DB_USER=dc-project
DB_PWD=dc-project
DB_PWD=dc-project
REDIS_PORT=6379
REDIS_CONNECTION=redis://redis:6379
REDIS_COMMANDER_PORT=8081
RABBITMQ_PORT=5672
RABBITMQ_CONNECTION=amqp://rabbitmq:5672
RABBITMQ_MANAGEMENT_PORT=15672
SEND_GRID_KEY=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

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

@@ -0,0 +1,123 @@
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: Tests
on:
pull_request:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt
- name: Cleanup Gradle Cache
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties
- name: processResources
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: processResources
- name: processTestResources
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: processResources
- uses: actions/upload-artifact@v2
with:
name: Build
path: build
testSql:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/download-artifact@v2
with:
name: Build
path: build
- name: TestSql
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: testSql
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/download-artifact@v2
with:
name: Build
path: build
- name: Test
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: test -x testSql
- name: Coverage
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
lint:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/download-artifact@v2
with:
name: Build
path: build
- name: Lint
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: ktlintCheck

7
.gitignore vendored
View File

@@ -1,8 +1,11 @@
/.gradle
/.idea
/.idea/*
!/.idea/runConfigurations
/out
/build
*.ipr
*.iws
dcproject.iml
/var
/var
GH_TOKEN.txt
allSQL.sql

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
dcproject

View File

@@ -1,16 +1,79 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value />
</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" />
</JetCodeStyleSettings>
<PostgresCodeStyleSettings version="2">
<option name="ALIAS_CASE" value="1" />
<SqlCodeStyleSettings version="5">
<option name="KEYWORD_CASE" value="1" />
<option name="TYPE_CASE" value="1" />
<option name="IDENTIFIER_CASE" value="1" />
</PostgresCodeStyleSettings>
<option name="TYPE_CASE" value="4" />
<option name="ALIAS_CASE" value="4" />
<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">
<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>
</code_scheme>
</component>

View File

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

13
.idea/dataSources.xml generated
View File

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

4
.idea/gradle.xml generated
View File

@@ -5,16 +5,14 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="PLATFORM" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="11" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>

11
.idea/misc.xml generated
View File

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

View File

@@ -1,22 +1,37 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<log_file alias="test.log" path="$PROJECT_DIR$/var/log/test" />
<module name="dcproject.test" />
<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="11" />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<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="directory" />
<option name="TEST_OBJECT" value="pattern" />
<option name="VM_PARAMETERS" value="-ea -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<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>

31
.idea/runConfigurations/Build.xml generated Normal file
View File

@@ -0,0 +1,31 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="build" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" />
</ENTRIES>
</extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,38 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build and start all Docker" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envFilePath" value="" />
<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="secondarySourceFiles">
<list>
<option value="docker-compose-sonar.yml" />
</list>
</option>
<option name="services">
<list>
<option value="app" />
<option value="db" />
<option value="elasticsearch" />
<option value="openapi" />
<option value="rabbitmq" />
<option value="redis" />
<option value="sonarqube" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,31 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build without test" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="build" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" />
</ENTRIES>
</extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,11 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="commandLineOptions" value="--build" />
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</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

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<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" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="tags" />
<option name="VM_PARAMETERS" value="-ea -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="functional" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<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" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="tags" />
<option name="VM_PARAMETERS" value="-ea -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="functional&amp;!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -0,0 +1,27 @@
<component name="ProjectRunConfigurationManager">
<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" />
<useClassPathOnly />
<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="integration..*" />
</patterns>
<tag value="!functional" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</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>

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

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

View File

@@ -1,6 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="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" />

View File

@@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RunCucumberTest" type="JUnit" factoryName="JUnit" nameIsGenerated="true">
<output_file path="$PROJECT_DIR$/var/log/test/cucumber.out.log" is_save="true" />
<log_file alias="cucumber.log" path="$PROJECT_DIR$/var/log/test" />
<module name="dcproject.test" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="testArticle" />
<option name="TEST_OBJECT" value="class" />
<option name="PARAMETERS" value="" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -0,0 +1,38 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run dependencies" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envFilePath" value="" />
<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="secondarySourceFiles">
<list>
<option value="docker-compose-sonar.yml" />
</list>
</option>
<option name="services">
<list>
<option value="db" />
<option value="elasticsearch" />
<option value="rabbitmq" />
<option value="redis" />
<option value="openapi" />
<option value="sonarqube" />
<option value="sonarqube_db" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

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

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

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<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

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

26
.idea/runConfigurations/Start_App.xml generated Normal file
View File

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

View File

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

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>

17
.idea/runConfigurations/Test_All_SQL.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<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="SCRIPT_PATH" value="$PROJECT_DIR$/src/test/sql/test.sh" />
<option name="SCRIPT_OPTIONS" value="1" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/src/test/sql" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="false" />
<option name="INTERPRETER_PATH" value="$PROJECT_DIR$/../../../../Program Files/Git/bin/bash.exe" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="false" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>

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

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

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

@@ -0,0 +1,28 @@
<component name="ProjectRunConfigurationManager">
<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" />
<useClassPathOnly />
<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..*" />
</patterns>
<tag value="!functional" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

6
.idea/vcs.xml generated
View File

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

59
Makefile Normal file
View File

@@ -0,0 +1,59 @@
.EXPORT_ALL_VARIABLES:
VERSION=$(shell ./hook/version.sh)
GITHUB_USERNAME=$(shell git config user.email)
GITHUB_TOKEN=$(shell cat ./GH_TOKEN.txt)
# HELP
# This will output the help for each task
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.PHONY: help
help: ## This help.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help
bd: build-docker
build-docker: ## Build the docker image of application (alias: bd)
docker build -t dc-project -f docker/app/Dockerfile .
pd: publish-docker
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)
@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 push docker.pkg.github.com/flecomte/dc-project/dc-project:${VERSION}
rd: run-docker
run-docker: ## Build and Run all docker services (alias: rd)
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
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)
gradlew publish
f: fixtures
fixtures: ## Import fixtures (alias: f)
bash src/main/resources/sql/fixtures/fixtures.sh
reset-database: ## Reset database !!!
cd src/main/resources/sql/ ; bash resetDB.sh
test-sql: ## Test sql
cd src/test/sql/ ; bash test.sh 1
v: version
version: ## Show current version (alias: v)
@echo ${VERSION}

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
# DC Project
[![CodeFactor](https://www.codefactor.io/repository/github/flecomte/dc-project/badge?s=869dc426625a253a07bea95f9380e23fdb048b94)](https://www.codefactor.io/repository/github/flecomte/dc-project)
[![Tests](https://github.com/flecomte/dc-project/actions/workflows/tests.yml/badge.svg)](https://github.com/flecomte/dc-project/actions/workflows/tests.yml)
[![Coverage Status](https://coveralls.io/repos/github/flecomte/dc-project/badge.svg?branch=master)](https://coveralls.io/github/flecomte/dc-project?branch=master)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/0ec4fe63370148ca956974f90f8d55be)](https://www.codacy.com/gh/flecomte/dc-project/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=flecomte/dc-project&amp;utm_campaign=Badge_Grade)
[Installation](./doc/installation/Installation.md)
### Run dockers
```bash
$ make run-docker
```
### Add fixtures
```bash
$ make fixtures
```
## Publish package
1. Got to [https://github.com](https://github.com/settings/tokens/new) and create a new token with packages scopes
2. Create a file `GH_TOKEN.txt` and put it the github token
### Maven
```bash
$ make publish-maven
```
### Docker
```bash
$ make publish-docker
```

View File

@@ -1,49 +1,374 @@
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
val koinVersion: String by project
val postgresjson_version: String by project
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.owasp.dependencycheck.reporting.ReportGenerator
import org.slf4j.LoggerFactory
plugins {
application
kotlin("jvm") version "1.3.40"
val ktorVersion = "1.5.0"
val kotlinVersion = "1.4.30"
val coroutinesVersion = "1.4.3"
val logbackVersion = "1.2.3"
val koinVersion = "2.0.1"
val jacksonVersion = "2.12.1"
group = "com.github.flecomte"
version = versioning.info.run {
if (dirty) {
versioning.info.full
} else {
versioning.info.lastTag
}
}
group = "fr.dcproject"
version = "0.0.1"
plugins {
jacoco
application
maven
id("maven-publish")
kotlin("jvm") version "1.4.30"
kotlin("plugin.serialization") version "1.4.30"
id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
id("org.owasp.dependencycheck") version "6.1.1"
id("org.sonarqube") version "3.1.1"
id("net.nemerosa.versioning") version "2.14.0"
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
id("com.avast.gradle.docker-compose") version "0.14.0"
id("com.github.kt3k.coveralls") version "2.8.4"
}
application {
mainClassName = "io.ktor.server.netty.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.2")
}
}
tasks.distZip.configure { enabled = false }
tasks.distTar.configure { enabled = false }
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
}
}
val migration by tasks.registering {
group = "application"
dependsOn(tasks.named("composeUp"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/resources/main/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/resources/main/sql/migrations").toURI(),
file("$buildDir/resources/main/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/resources/test/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/resources/main/sql/migrations").toURI(),
file("$buildDir/resources/main/sql/functions").toURI()
).run {
run()
connection.disconnect()
}
}
}
val testSql by tasks.registering {
group = "verification"
dependsOn(tasks.named("processResources"))
dependsOn(tasks.named("processTestResources"))
dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
val connection = Connection(
host = config.getString("db.host"),
port = config.getInt("db.port"),
database = config.getString("db.database"),
username = config.getString("db.username"),
password = config.getString("db.password")
)
Migrations(
connection,
file("$buildDir/resources/main/sql/migrations").toURI(),
file("$buildDir/resources/main/sql/functions").toURI(),
file("$buildDir/resources/test/sql/fixtures").toURI()
).run()
Requester.RequesterFactory(
connection = connection,
queriesDirectory = file("$buildDir/resources/test/sql").toURI()
).createRequester().run {
getQueries().map {
try {
it.sendQuery() == 0
} catch (e: Exception) {
false
}
}
}
connection.disconnect()
}
}
tasks.withType<Jar> {
manifest {
attributes(
mapOf(
"Main-Class" to application.mainClassName
)
)
}
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
sourceCompatibility = "11"
targetCompatibility = "11"
}
}
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")
from(sourceSets.getByName("main").allSource)
}
tasks.test {
useJUnit()
useJUnitPlatform()
systemProperty("junit.jupiter.execution.parallel.enabled", true)
dependsOn(testSql)
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
}
coveralls {
sourceDirs.add("src/main/kotlin")
}
tasks.register("testAll") {
group = "verification"
dependsOn(testSql)
dependsOn(tasks.test)
dependsOn(tasks.ktlintCheck)
}
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 {
if (versioning.info.dirty == false) {
repositories {
maven {
name = "dc-project"
group = "com.github.flecomte"
url = uri("https://maven.pkg.github.com/flecomte/dc-project")
credentials {
username = System.getenv("GITHUB_USERNAME")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("dc-project") {
from(components["java"])
artifact(sourcesJar)
}
}
} else {
LoggerFactory.getLogger("gradle")
.warn("The git is DIRTY (${versioning.info.full})")
}
}
jacoco {
toolVersion = "0.8.6"
applyTo(tasks.run.get())
}
tasks.register<JacocoReport>("applicationCodeCoverageReport") {
executionData(tasks.run.get())
sourceSets(sourceSets.main.get())
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.isEnabled = true
html.isEnabled = true
}
}
detekt {
buildUponDefaultConfig = true // preconfigure defaults
ignoreFailures = true
// config = files("$projectDir/config/detekt.yml") // point to your custom config defining rules to run, overwriting default behavior
// baseline = file("$projectDir/config/baseline.xml") // a way of suppressing issues before introducing detekt
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"
ignoreFailures = true
}
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 {
if (OperatingSystem.current().isWindows) {
dependsOn(setMaxMapCount)
}
}
dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
}
repositories {
mavenLocal()
jcenter()
maven { url = uri("https://kotlin.bintray.com/ktor") }
maven { url = uri("https://jitpack.io") }
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-locations:$ktor_version")
implementation("io.ktor:ktor-auth:$ktor_version")
implementation("io.ktor:ktor-auth-jwt:$ktor_version")
implementation("io.ktor:ktor-gson:$ktor_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-reactor:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
implementation("io.ktor:ktor-server-jetty:$ktorVersion")
implementation("io.ktor:ktor-client-jetty:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logbackVersion")
implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-locations:$ktorVersion")
implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktorVersion")
implementation("io.ktor:ktor-websockets:$ktorVersion")
implementation("org.koin:koin-ktor:$koinVersion")
implementation("io.ktor:ktor-jackson:$ktor_version")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.9")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.9.9")
implementation("net.pearx.kasechange:kasechange-jvm:1.1.0")
implementation("fr.postgresjson:postgresjson-jdbc:$postgresjson_version")
testImplementation("io.ktor:ktor-server-tests:$ktor_version")
testImplementation("io.ktor:ktor-client-mock:$ktor_version")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktor_version")
implementation("io.ktor:ktor-jackson:$ktorVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion")
implementation("net.pearx.kasechange:kasechange-jvm:1.3.0")
implementation("com.auth0:java-jwt:3.12.0")
implementation("com.github.jasync-sql:jasync-postgresql:1.1.6")
implementation("com.github.flecomte:postgres-json:2.1.2")
implementation("com.sendgrid:sendgrid-java:4.7.1")
implementation("io.lettuce:lettuce-core:5.3.6.RELEASE") // TODO update to 6.0.2
implementation("com.rabbitmq:amqp-client:5.10.0")
implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
implementation("com.jayway.jsonpath:json-path:2.5.0")
implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.0")
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion")
testImplementation("org.koin:koin-test:$koinVersion")
testImplementation("io.mockk:mockk:1.9")
testImplementation("org.junit.jupiter:junit-jupiter:5.5.0")
testImplementation("org.amshove.kluent:kluent:1.4")
testImplementation("io.cucumber:cucumber-java8:4.3.1")
testImplementation("io.cucumber:cucumber-junit:4.3.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
testImplementation("org.amshove.kluent:kluent:1.61")
testImplementation("io.mockk:mockk-agent-api:1.10.6")
testImplementation("io.mockk:mockk-agent-jvm:1.10.6")
testImplementation("org.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")
}

View File

@@ -0,0 +1,20 @@
Installation on Linux (debian)
=====================
## Prerequisites
```bash
apt-get update
```
### Install docker
See: [docs.docker.com/get-docker](https://docs.docker.com/get-docker/)
```bash
apt-get install docker
apt-get install docker-compose
```
### Install Git
See: [git-scm.com/downloads](https://git-scm.com/downloads)
```powershell
apt-get install git
```

View File

@@ -0,0 +1,50 @@
Installation on Windows
============
## Prerequisites
### Install chocolaty (optional)
First install Chocolaty => [https://chocolatey.org/install](https://chocolatey.org/install)
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
```
### Install make (optional)
Install make with chocolaty
```powershell
chocolatey install make
```
Install make for gitbash
[https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058#make]()
### Install awk (optional)
Install awk with chocolaty
```powershell
chocolatey install awk
```
### Install docker
See: [docs.docker.com/get-docker](https://docs.docker.com/get-docker/)
```powershell
choco install docker-desktop
```
If you use docker with WSL2, you need to change config for elasticsearch:
```powershell
wsl -d docker-desktop
sysctl -w vm.max_map_count=262144
echo "vm.max_map_count = 262144" > /etc/sysctl.d/99-docker-desktop.conf
```
then restart docker-desktop
### Install Git
See: [git-scm.com/downloads](https://git-scm.com/downloads)
```powershell
choco install git
```
### Add JAVA_HOME path
In CMD (Not Powershell)
```cmd
$ setx -m JAVA_HOME "C:\Users\%USERNAME%\.jdks\corretto-11.0.7"
$ setx -m PATH "%PATH%;%JAVA_HOME%\bin";
```

View File

@@ -0,0 +1,7 @@
Installation
============
1. [Installation on Windows](Installation-windows.md)
2. [Installation on Linux](Installation-linux.md)
3. [Problems in installations](Problems.md)

View File

@@ -0,0 +1,29 @@
Problems
========
1. [max_map_count](#max_map_count)
max_map_count
-------------
**Context**:
Elasticsearch in Docker on WSL2
**Error**:
```
max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
```
**Resolution**:
[Stackoverflow](https://stackoverflow.com/questions/42111566/elasticsearch-in-windows-docker-image-vm-max-map-count)
In Powershell:
```powershell
wsl -d docker-desktop
sysctl -w vm.max_map_count=262144
echo "vm.max_map_count = 262144" > /etc/sysctl.d/99-docker-desktop.conf
```
restart docker-desktop

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,21 +1,65 @@
# To execute this docker-compose yml file use docker-compose -f <file_name> up
# Add the "-d" flag at the end for detached execution
version: '3.7'
version: '3.8'
services:
openapi:
container_name: ${APP_NAME}_openapi
image: swaggerapi/swagger-ui
ports:
- ${OPENAPI_PORT}:8080
environment:
URL: "http://localhost:8080"
rabbitmq:
container_name: ${APP_NAME}_rabbitmq
image: rabbitmq:management-alpine
ports:
- ${RABBITMQ_PORT}:5672
- ${RABBITMQ_MANAGEMENT_PORT}:15672
redis:
container_name: ${APP_NAME}_redis
image: redis:6-alpine
ports:
- ${REDIS_PORT}:6379
volumes:
- redis-data:/var/lib/redis:rw
app:
container_name: ${APP_NAME}_app
build:
context: .
dockerfile: docker/app/Dockerfile
ports:
- ${APP_PORT}:8080
environment:
DB_HOST: ${DB_HOST}
SEND_GRID_KEY: ${SEND_GRID_KEY}
REDIS_CONNECTION: ${REDIS_CONNECTION}
RABBITMQ_CONNECTION: ${RABBITMQ_CONNECTION}
ELASTICSEARCH_CONNECTION: ${ELASTICSEARCH_CONNECTION}
depends_on:
- elasticsearch
- db
- redis
- rabbitmq
elasticsearch:
container_name: elasticsearch_${NAME}
container_name: ${APP_NAME}_elasticsearch
image: elasticsearch:6.7.1
ports:
- ${ELASTIC_REST}:9200
- ${ELASTIC_NODES}:9300
healthcheck:
test: ["CMD", "curl", "-f", "http://elasticsearch:9200"]
interval: 3s
timeout: 2s
retries: 20
# Database
db:
container_name: postgresql_${NAME}
container_name: ${APP_NAME}_postgresql
build:
context: docker/postgresql
restart: always
ports:
- ${POSTGRESQL_PORT}:5432
environment:
@@ -24,11 +68,15 @@ services:
POSTGRES_DB: ${DB_PWD}
volumes:
- ./var/log/postgresql:/var/log/postgresql:rw
- ./var/postgresql/data:/var/lib/postgresql/data:rw
- db-data:/var/lib/postgresql/data:rw
depends_on:
- elasticsearch
healthcheck:
test: ["CMD", "curl", "-f", "http://elasticsearch:9200/"]
test: [ "CMD", "pg_isready", "-q", "-d", "${DB_NAME}", "-U", "${DB_USER}" ]
interval: 3s
timeout: 2s
retries: 20
volumes:
db-data:
redis-data:

22
docker/app/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
#### BUILD ####
FROM gradle:6.8-jdk11 AS build
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck --no-daemon
RUN gradle shadowJar
#### RUN ####
FROM amazoncorretto:11-alpine as run
ENV APPLICATION_USER ktor
RUN adduser -D -g '' $APPLICATION_USER
RUN mkdir /app
RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER
COPY --from=build /home/gradle/src/build/libs/dcproject-latest-all.jar /app/dcproject.jar
WORKDIR /app
CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:InitialRAMFraction=2", "-XX:MinRAMFraction=2", "-XX:MaxRAMFraction=2", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "dcproject.jar"]

View File

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

View File

@@ -1,6 +1,9 @@
ktor_version=1.2.2
kotlin.code.style=official
kotlin_version=1.3.40
logback_version=1.2.1
postgresjson_version=0.1
koinVersion=2.0.1
systemProp.sonar.host.url=http://localhost:9002
systemProp.sonar.login=admin
systemProp.sonar.password=sonar
systemProp.sonar.projectKey=dc-project
systemProp.sonar.projectName=DC Project
systemProp.sonar.java.coveragePlugin=jacoco
systemProp.sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
systemProp.sonar.kotlin.detekt.reportPaths=build/reports/detekt/detekt.xml

Binary file not shown.

View File

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

53
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -66,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -109,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -138,19 +156,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# 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"
# 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" "$@"

43
gradlew.bat vendored
View File

@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
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.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -35,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -45,28 +64,14 @@ echo location of your Java installation.
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
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@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
@rem End local scope for the variables with windows NT shell

12
hook/version.sh Normal file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
if [[ $(git describe --tags --dirty) =~ ^V?([0-9][0-9.]*(-dirty)?)$ ]]; then
VERSION="${BASH_REMATCH[1]}"
elif [[ $(git describe --tags --dirty) =~ ^V?([0-9][0-9.]*)-([0-9]+)-g(.+(-dirty)?)$ ]]; then
VERSION="${BASH_REMATCH[1]}-${BASH_REMATCH[3]}"
else
exit 1
fi
echo $VERSION

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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

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

View File

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

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

View File

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

@@ -0,0 +1,14 @@
package fr.dcproject.routes
interface PaginatedRequestI {
val page: Int
val limit: Int
}
open class PaginatedRequest(
page: Int = 1,
limit: Int = 50
) : PaginatedRequestI {
override val page: Int = if (page < 1) 1 else page
override val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}

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

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

View File

@@ -0,0 +1,10 @@
package fr.dcproject.common.utils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
internal class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> {
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
}

View File

@@ -0,0 +1,11 @@
package fr.dcproject.common.utils
import java.util.UUID
fun String.toUUID(): UUID = UUID.fromString(this.trim())
fun List<String?>.toUUID(): List<UUID> = this
.filterNotNull()
.map { it.trim() }
.filter { it.isNotBlank() }
.map { UUID.fromString(it) }

View File

@@ -0,0 +1,28 @@
package fr.dcproject.common.utils
import org.elasticsearch.client.Request
import org.elasticsearch.client.RestClient
import org.slf4j.Logger
import org.slf4j.LoggerFactory
fun RestClient.waitElasticsearchIsUp() {
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.elasticsearch")
val request = Request("GET", "/_cluster/health")
repeat(5 * 60 / 2) { // 5 minutes
runCatching {
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")
}

View File

@@ -0,0 +1,52 @@
package fr.dcproject.component.article
import fr.dcproject.common.entity.CreatedBy
import fr.dcproject.common.entity.VersionableId
import fr.dcproject.common.security.AccessControl
import fr.dcproject.common.security.AccessResponse
import fr.dcproject.component.article.database.ArticleAuthI
import fr.dcproject.component.article.database.ArticleI
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.citizen.database.CitizenI
class ArticleAccessControl(private val articleRepo: ArticleRepository) : AccessControl() {
fun <S : ArticleAuthI<*>> canView(subjects: List<S>, citizen: CitizenI?): AccessResponse =
canAll(subjects) { canView(it, citizen) }
fun <S : ArticleAuthI<*>> canView(subject: S, citizen: CitizenI?): AccessResponse {
return if (subject.isDeleted()) denied("Article is deleted", "article.deleted")
else if (subject.draft && (citizen == null || subject.createdBy.id != citizen.id)) denied("Article is draft, but it's not yours", "article.draft.not.yours")
else granted()
}
fun <S : CreatedBy<*>> canDelete(subject: S, citizen: CitizenI?): AccessResponse {
if (citizen == null) return denied("You must be connected to create article", "article.create.notConnected")
return if (subject.createdBy.id == citizen.id) {
granted()
} else {
denied("Cannot delete article if is not yours", "article.delete.notYours")
}
}
fun <S> canUpsert(subject: S, citizen: CitizenI?): AccessResponse
where S : ArticleI,
S : CreatedBy<*>,
S : VersionableId {
if (citizen == null) return denied("You must be connected to create article", "article.create.notConnected")
/* The new Article must by created by the same citizen of the connected citizen */
if (subject.createdBy.id == citizen.id) {
/* The creator must be the same of the creator of preview version of article */
val lastVersionId = articleRepo
.findVersionsByVersionId(1, 1, subject.versionId)
.result
.firstOrNull()?.createdBy?.id
return when (lastVersionId) {
null -> granted("You can create a new Article")
citizen.id -> granted("Last version is yours")
else -> denied("Last version is not yours", "article.lastVersion.notYours")
}
}
return denied("This article must be yours for update it", "article.update.notYours")
}
}

View File

@@ -0,0 +1,101 @@
package fr.dcproject.component.article
import fr.dcproject.common.entity.VersionableId
import fr.dcproject.common.utils.contentToString
import fr.dcproject.common.utils.getJsonField
import fr.dcproject.common.utils.toIso
import fr.dcproject.component.article.database.ArticleI
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.views.ViewManager
import fr.dcproject.component.views.entity.ViewAggregation
import org.elasticsearch.client.Request
import org.elasticsearch.client.Response
import org.elasticsearch.client.RestClient
import org.joda.time.DateTime
import java.util.UUID
/**
* Wrapper for manage views with elasticsearch
*/
class ArticleViewManager <A> (private val restClient: RestClient) : ViewManager<A> where A : VersionableId, A : ArticleI {
/**
* Add view on article to elasticsearch
*/
override fun addView(ip: String, entity: A, citizen: CitizenI?, dateTime: DateTime): Response? {
val isLogged = (citizen != null).toString()
val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!!
val request = Request(
"POST",
"/views/_doc/"
).apply {
//language=JSON
setJsonEntity(
"""
{
"logged": $isLogged,
"type": "article",
"user_ref": "$ref",
"ip": "$ip",
"id": "${entity.id}",
"version_id": "${entity.versionId}",
"citizen_id": "${citizen?.id}",
"view_at": "${dateTime.toIso()}"
}
""".trimIndent()
)
}
return restClient.performRequest(request)
}
/**
* Get article views aggregations from elasticsearch
*/
override fun getViewsCount(entity: A): ViewAggregation {
val request = Request(
"GET",
"/views/_search"
).apply {
//language=JSON
setJsonEntity(
"""
{
"size": 0,
"query": {
"bool": {
"must": {
"term": {
"version_id": "${entity.versionId}"
}
}
}
},
"aggs" : {
"total": {
"composite" : {
"sources" : [
{ "version_id": { "terms": {"field": "version_id" } } }
]
}
},
"unique" : {
"cardinality" : {
"field" : "user_ref",
"precision_threshold": 1
}
}
}
}
""".trimIndent()
)
}
return restClient
.performRequest(request).contentToString().run {
ViewAggregation(
getJsonField("$.aggregations.total.buckets[0].doc_count") ?: 0,
getJsonField("$.aggregations.unique.value") ?: 0
)
}
}
}

View File

@@ -0,0 +1,9 @@
package fr.dcproject.component.article
import fr.dcproject.component.article.database.ArticleRepository
import org.koin.dsl.module
val articleKoinModule = module {
single { ArticleRepository(get()) }
single { ArticleAccessControl(get()) }
}

View File

@@ -0,0 +1,111 @@
package fr.dcproject.component.article.database
import fr.dcproject.common.entity.CreatedAt
import fr.dcproject.common.entity.CreatedBy
import fr.dcproject.common.entity.DeletedAt
import fr.dcproject.common.entity.EntityI
import fr.dcproject.common.entity.TargetI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.entity.Versionable
import fr.dcproject.common.entity.VersionableId
import fr.dcproject.component.citizen.database.CitizenCartI
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.opinion.entity.Opinionable
import fr.dcproject.component.opinion.entity.Opinions
import fr.dcproject.component.vote.entity.Votable
import fr.dcproject.component.vote.entity.VotableImp
import fr.dcproject.component.workgroup.database.WorkgroupCart
import fr.dcproject.component.workgroup.database.WorkgroupCartI
import fr.dcproject.component.workgroup.database.WorkgroupRef
import org.joda.time.DateTime
import java.util.UUID
data class ArticleForView(
override val id: UUID = UUID.randomUUID(),
override val title: String,
val anonymous: Boolean = true,
val content: String,
val description: String,
val tags: List<String> = emptyList(),
override val createdBy: CitizenCreator,
override val versionNumber: Int = 0,
override val versionId: UUID = UUID.randomUUID(),
val workgroup: WorkgroupCart? = null,
override val opinions: Opinions = emptyMap(),
override val draft: Boolean = false,
override val deletedAt: DateTime? = null
) : ArticleRef(id),
ArticleAuthI<CitizenCreator>,
ArticleWithTitleI,
Versionable,
CreatedAt by CreatedAt.Imp(),
DeletedAt by DeletedAt.Imp(deletedAt),
VersionableId,
Opinionable,
Votable by VotableImp() {
val lastVersion: Boolean = false
}
interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, VersionableId, TargetI, CreatedBy<C> {
val anonymous: Boolean
val content: String
val description: String
val draft: Boolean
val workgroup: WorkgroupRef?
}
class ArticleForUpdate(
override val id: UUID = UUID.randomUUID(),
override val title: String,
override val anonymous: Boolean = true,
override val content: String,
override val description: String,
tags: List<String> = emptyList(),
override val draft: Boolean = false,
override val createdBy: CitizenRef,
override val workgroup: WorkgroupRef? = null,
override val versionId: UUID = UUID.randomUUID(),
override val deletedAt: DateTime? = null,
) : ArticleRef(id),
ArticleForUpdateI<CitizenRef>,
ArticleAuthI<CitizenRef>,
VersionableId {
val tags: List<String> = tags.distinct()
}
class ArticleForListing(
id: UUID? = null,
override val title: String,
override val createdBy: CitizenCreator,
override val workgroup: WorkgroupCart? = null,
override val deletedAt: DateTime? = null,
override val draft: Boolean = false,
val lastVersion: Boolean = false
) : ArticleForListingI,
ArticleRef(id),
ArticleAuthI<CitizenCartI>,
Votable by VotableImp(),
CreatedBy<CitizenCartI>
interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {
val workgroup: WorkgroupCartI?
}
open class ArticleRef(
id: UUID? = null
) : ArticleI, TargetRef(id)
interface ArticleI : EntityI, TargetI
interface ArticleWithTitleI : ArticleI {
val title: String
}
interface ArticleAuthI<U : CitizenI> :
ArticleI,
CreatedBy<U>,
DeletedAt {
val draft: Boolean
}

View File

@@ -0,0 +1,58 @@
package fr.dcproject.component.article.database
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter
import fr.postgresjson.repository.RepositoryI
import net.pearx.kasechange.toSnakeCase
import java.util.UUID
class ArticleRepository(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): ArticleForView? {
val function = requester.getFunction("find_article_by_id")
return function.selectOne("id" to id)
}
fun findVersionsById(page: Int = 1, limit: Int = 50, id: UUID): Paginated<ArticleForListing> {
return requester
.getFunction("find_articles_versions_by_id")
.select(page, limit, "id" to id)
}
fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForListing> {
return requester
.getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to versionId)
}
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: RepositoryI.Direction? = null,
search: String? = null,
filter: Filter = Filter()
): Paginated<ArticleForListing> {
return requester
.getFunction("find_articles")
.select(
page,
limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search,
"filter" to filter
)
}
fun upsert(article: ArticleForUpdate): ArticleForView? {
return requester
.getFunction("upsert_article")
.selectOne("resource" to article)
}
class Filter(
val createdById: String? = null,
val workgroupId: String? = null
) : Parameter
}

View File

@@ -0,0 +1,72 @@
package fr.dcproject.component.article.routes
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.database.ArticleForListing
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.auth.citizenOrNull
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object FindArticleVersions {
@Location("/articles/{article}/versions")
class ArticleVersionsRequest(
article: UUID,
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null
) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
val article = ArticleRef(article)
}
private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) =
findVersionsById(request.page, request.limit, request.article.id)
fun Route.findArticleVersions(repo: ArticleRepository, ac: ArticleAccessControl) {
get<ArticleVersionsRequest> {
repo.findVersions(it)
.apply { ac.assert { canView(result, citizenOrNull) } }
.run {
call.respond(
toOutput { a: ArticleForListing ->
object {
val id = a.id
val title = a.title
val createdBy = object {
val id = a.createdBy.id
val name = a.createdBy.name.let { n ->
object {
val firstName = n.firstName
val lastName = n.lastName
}
}
val email = a.createdBy.email
}
val workgroup = a.workgroup?.let { w ->
object {
val id = w.id
val name = w.name
}
}
val draft = a.draft
val lastVersion = a.lastVersion
}
}
)
}
}
}
}

View File

@@ -0,0 +1,68 @@
package fr.dcproject.component.article.routes
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.database.ArticleForListing
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object FindArticles {
@Location("/articles")
class ArticlesRequest(
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null,
val createdBy: String? = null,
val workgroup: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit)
private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<ArticleForListing> {
return find(
request.page,
request.limit,
request.sort,
request.direction,
request.search,
ArticleRepository.Filter(createdById = request.createdBy, workgroupId = request.workgroup)
)
}
fun Route.findArticles(repo: ArticleRepository, ac: ArticleAccessControl) {
get<ArticlesRequest> {
repo.findArticles(it)
.apply { ac.assert { canView(result, citizenOrNull) } }
.let {
call.respond(
it.toOutput {
object {
val id = it.id
val title = it.title
val createdBy: Any = it.createdBy.toOutput()
val workgroup = it.workgroup?.let {
object {
val id = it.id
val name = it.name
}
}
val draft = it.draft
}
}
)
}
}
}
}

View File

@@ -0,0 +1,83 @@
package fr.dcproject.component.article.routes
import fr.dcproject.common.security.assert
import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.auth.citizenOrNull
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import kotlinx.coroutines.launch
import java.util.UUID
@KtorExperimentalLocationsAPI
object GetOneArticle {
@Location("/articles/{article}")
class ArticleRequest(article: UUID) {
val article = ArticleRef(article)
}
fun Route.getOneArticle(viewManager: ArticleViewManager<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
get<ArticleRequest> {
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
ac.assert { canView(article, citizenOrNull) }
call.respond(
article.let { a ->
object {
val id = a.id
val versionId = a.versionId
val versionNumber = a.versionNumber
val title = a.title
val anonymous = a.anonymous
val content = a.content
val description = a.description
val tags = a.tags
val draft = a.draft
val lastVersion = a.lastVersion
val createdAt = a.createdAt
val createdBy: Any = object {
val id: UUID = a.createdBy.id
val name: Any = object {
val firstName: String = a.createdBy.name.firstName
val lastName: String = a.createdBy.name.lastName
}
val email: String = a.createdBy.email
}
val workgroup: Any? = a.workgroup?.let { w ->
object {
val id: UUID = w.id
val name: String = w.name
}
}
val votes: Any = object {
val up: Int = a.votes.up
val neutral: Int = a.votes.neutral
val down: Int = a.votes.down
val total: Int = a.votes.total
val score: Int = a.votes.score
}
val views: Any = viewManager.getViewsCount(article).let { v ->
object {
val total = v.total
val unique = v.unique
}
}
val opinions: Map<String, Int> = a.opinions
}
}
)
launch {
viewManager.addView(call.request.local.remoteHost, article, citizenOrNull)
}
}
}
}

View File

@@ -0,0 +1,71 @@
package fr.dcproject.component.article.routes
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.database.ArticleForUpdate
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.Input
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.workgroup.database.WorkgroupRef
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object UpsertArticle {
@Location("/articles")
class UpsertArticleRequest {
class Input(
val id: UUID?,
val title: String,
val anonymous: Boolean = true,
val content: String,
val description: String,
val tags: List<String> = emptyList(),
val draft: Boolean = false,
val versionId: UUID,
val workgroup: WorkgroupRef? = null,
)
}
fun Route.upsertArticle(repo: ArticleRepository, publisher: Publisher, ac: ArticleAccessControl) {
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest<Input>().run {
ArticleForUpdate(
id = id ?: UUID.randomUUID(),
title = title,
anonymous = anonymous,
content = content,
description = description,
tags = tags,
draft = draft,
createdBy = citizen,
workgroup = workgroup,
versionId = versionId
)
}
post<UpsertArticleRequest> {
val article = call.convertRequestToEntity()
ac.assert { canUpsert(article, citizenOrNull) }
repo.upsert(article)?.let { a ->
call.respond(
object {
val id: UUID = a.id
val versionId = a.versionId
val versionNumber = a.versionNumber
}
)
publisher.publish(ArticleUpdateNotification(a))
} ?: error("Article not updated")
}
}
}

View File

@@ -0,0 +1,20 @@
package fr.dcproject.component.article.routes
import fr.dcproject.component.article.routes.FindArticleVersions.findArticleVersions
import fr.dcproject.component.article.routes.FindArticles.findArticles
import fr.dcproject.component.article.routes.GetOneArticle.getOneArticle
import fr.dcproject.component.article.routes.UpsertArticle.upsertArticle
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installArticleRoutes() {
authenticate(optional = true) {
findArticles(get(), get())
findArticleVersions(get(), get())
getOneArticle(get(), get(), get())
upsertArticle(get(), get(), get())
}
}

View File

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

View File

@@ -0,0 +1,15 @@
package fr.dcproject.component.auth
import fr.dcproject.application.Configuration
import fr.dcproject.common.email.Mailer
import fr.dcproject.component.auth.database.UserRepository
import org.koin.dsl.module
val authKoinModule = module {
single { UserRepository(get()) }
// Used to send a connexion link by email
single {
val config: Configuration = get()
PasswordlessAuth(get<Mailer>(), config.domain, get())
}
}

View File

@@ -0,0 +1,57 @@
package fr.dcproject.component.auth
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.common.email.Mailer
import fr.dcproject.component.auth.jwt.makeToken
import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.component.citizen.database.CitizenWithEmail
import fr.dcproject.component.citizen.database.CitizenWithUserI
import io.ktor.http.URLBuilder
/**
* Send a connexion link by email
*/
class PasswordlessAuth(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository
) {
fun sendEmail(email: String, url: String) {
val citizen = citizenRepo.findByEmail(email) ?: noEmail(email)
sendEmail(citizen, url)
}
fun <C> sendEmail(citizen: C, url: String) where C : CitizenWithEmail, C : CitizenWithUserI {
mailer.sendEmail {
val token = citizen.user.makeToken()
Mail(
Email("passwordless-auth@$domain"),
"Connection",
Email(citizen.email),
Content("text/plain", generateContent(token, url))
).apply {
addContent(Content("text/html", generateHtmlContent(token, url)))
}
}
}
private fun generateHtmlContent(token: String, url: String): String? {
val urlObject = URLBuilder(url)
urlObject.parameters.append("token", token)
return "Click <a href=\"${urlObject.buildString()}\">here</a> for connect to $domain"
}
private fun generateContent(token: String, url: String): String {
val urlObject = URLBuilder(url)
urlObject.parameters.append("token", token)
return "Copy this link into your browser for connect to $domain: \n${urlObject.buildString()}"
}
class EmailNotFound(val email: String) : Exception() {
override val message: String = "No Citizen with this email : $email"
}
private fun noEmail(email: String): Nothing = throw EmailNotFound(email)
}

View File

@@ -0,0 +1,61 @@
package fr.dcproject.component.auth.database
import fr.dcproject.common.entity.CreatedAt
import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.EntityI
import fr.dcproject.common.entity.UpdatedAt
import fr.dcproject.component.auth.database.UserI.Roles
import io.ktor.auth.Principal
import org.joda.time.DateTime
import java.util.UUID
class UserForCreate(
id: UUID = UUID.randomUUID(),
username: String,
override val password: String,
blockedAt: DateTime? = null,
roles: List<Roles> = emptyList()
) : User(id, username, blockedAt, roles),
UserWithPasswordI
open class User(
id: UUID = UUID.randomUUID(),
override var username: String,
var blockedAt: DateTime? = null,
var roles: List<Roles> = emptyList()
) : UserRef(id),
UserWithUsername,
CreatedAt by CreatedAt.Imp(),
UpdatedAt by UpdatedAt.Imp()
class UserCreator(
id: UUID = UUID.randomUUID(),
override val username: String,
) : UserRef(id), UserWithUsername
interface UserWithUsername : UserI {
val username: String
}
interface UserWithPasswordI : UserI {
val password: String
}
class UserWithPassword(
id: UUID,
override val password: String,
) : UserWithPasswordI,
UserRef(id)
open class UserRef(
id: UUID = UUID.randomUUID()
) : UserI, Entity(id)
interface UserI : EntityI, Principal {
enum class Roles { ROLE_USER, ROLE_ADMIN }
}
interface UserForAuthI : UserI {
var roles: List<Roles>
var blockedAt: DateTime?
}

View File

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

View File

@@ -0,0 +1,14 @@
package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWT
import fr.dcproject.component.auth.database.UserI
/**
* Produce a token for this combination of User and Account
*/
fun UserI.makeToken(): String = JWT.create()
.withSubject("Authentication")
.withIssuer(JwtConfig.issuer)
.withClaim("id", id.toString())
.withExpiresAt(JwtConfig.getExpiration())
.sign(JwtConfig.algorithm)

View File

@@ -0,0 +1,25 @@
package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import java.util.Date
object JwtConfig {
private const val secret = "zAP5MBA4B4Ijz0MZaS48"
const val issuer = "dc-project.fr"
private const val validityInMs = 3_600_000 * 10 // 10 hours
// TODO change to RSA512
val algorithm: Algorithm = Algorithm.HMAC512(secret)
val verifier: JWTVerifier = JWT
.require(algorithm)
.withIssuer(issuer)
.build()
/**
* Calculate the expiration Date based on current time + the given validity
*/
fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
}

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