250 Commits

Author SHA1 Message Date
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
270 changed files with 13090 additions and 1177 deletions

13
.env
View File

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

3
.gitignore vendored
View File

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

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
dcproject

View File

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

6
.idea/dataSources.xml generated
View File

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

3
.idea/gradle.xml generated
View File

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

8
.idea/misc.xml generated
View File

@@ -1,7 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="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" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View File

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

View File

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

View File

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

View File

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

24
.idea/runConfigurations/Auth_Tests.xml generated Normal file
View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

24
.idea/runConfigurations/Follow_Tests.xml generated Normal file
View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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>

24
.idea/runConfigurations/Start_Db.xml generated Normal file
View File

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

View File

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

View File

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

View File

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

24
.idea/runConfigurations/Start_Redis.xml generated Normal file
View File

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

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

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

24
.idea/runConfigurations/Vote_Tests.xml generated Normal file
View File

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

29
.idea/runConfigurations/Voter_Tests.xml generated Normal file
View File

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

View File

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

6
.idea/vcs.xml generated
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>

View File

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

View File

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

14
docker/app/Dockerfile Normal file
View File

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

View File

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

View File

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

Binary file not shown.

View File

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

22
gradlew vendored
View File

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

18
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 @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS= set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome

View File

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

View File

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

View File

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

135
src/main/kotlin/Module.kt Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
package fr.dcproject.messages
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.entity.*
import fr.postgresjson.entity.immutable.UuidEntityI
import java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Article as ArticleRepository
class NotificationEmailSender(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository,
private val articleRepo: ArticleRepository
) {
fun sendEmail(follow: FollowSimple<out TargetRef, CitizenRef>) {
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
val target = when (follow.target.reference) {
"article" ->
articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
else -> noTarget(follow.target.id)
}
val subject = when (follow.target.reference) {
"article" -> """New version for article "${target.title}""""
else -> "Notification"
}
mailer.sendEmail {
Mail(
Email("notification@$domain"),
subject,
Email(citizen.email),
Content("text/plain", generateContent(citizen, target))
).apply {
addContent(Content("text/html", generateHtmlContent(citizen, target)))
}
}
}
private fun generateHtmlContent(citizen: CitizenBasicI, target: UuidEntityI): String? {
return when (target) {
is Article -> """
Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent()
else -> noTarget(target.id)
}
}
private fun generateContent(citizen: CitizenBasicI, target: UuidEntityI): String {
return when (target) {
is Article -> """
Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent()
else -> noTarget(target.id)
}
}
class NoCitizen(message: String) : Exception(message)
class NoTarget(message: String) : Exception(message)
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,83 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Article
import fr.dcproject.entity.Citizen
import fr.dcproject.repository.CommentArticle.Sort
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
import fr.ktorVoter.assertCan
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.repository.CommentArticle as CommentArticleRepository
@KtorExperimentalLocationsAPI
object CommentArticlePaths {
@Location("/articles/{article}/comments")
class ArticleCommentRequest(
val article: Article,
page: Int = 1,
limit: Int = 50,
val search: String? = null,
sort: String = Sort.CREATED_AT.sql
) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
val sort: Sort = Sort.fromString(sort) ?: Sort.CREATED_AT
}
@Location("/articles/{article}/comments")
class PostArticleCommentRequest(
val article: Article
) {
class Comment(
val content: String
)
suspend fun getComment(call: ApplicationCall) = call.receive<Comment>().run {
CommentEntity(
target = article,
createdBy = call.citizen,
content = content
)
}
}
@Location("/citizens/{citizen}/comments/articles")
class CitizenCommentArticleRequest(val citizen: Citizen)
}
@KtorExperimentalLocationsAPI
fun Route.commentArticle(repo: CommentArticleRepository) {
get<CommentArticlePaths.ArticleCommentRequest> {
val comment = repo.findByTarget(it.article, it.page, it.limit, it.sort)
if (comment.result.isNotEmpty()) {
assertCan(VIEW, comment.result)
}
call.respond(HttpStatusCode.OK, comment)
}
post<CommentArticlePaths.PostArticleCommentRequest> {
it.getComment(call).let { comment ->
assertCan(CREATE, comment)
repo.comment(comment)
call.respond(HttpStatusCode.Created, comment)
}
}
get<CommentArticlePaths.CitizenCommentArticleRequest> {
repo.findByCitizen(it.citizen).let { comments ->
assertCan(VIEW, comments.result)
call.respond(comments)
}
}
}

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