536 Commits

Author SHA1 Message Date
72f8a27272 Merge pull request 'fix: cache refresh v2' (#123) from dev into master
Reviewed-on: #123
2026-03-23 21:12:00 +00:00
d3805ea0e7 fix: cache refresh v2
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m13s
2026-03-23 22:11:42 +01:00
a1e5006e95 Merge pull request 'fix: cache refresh' (#122) from dev into master
Reviewed-on: #122
2026-03-23 15:31:19 +00:00
2d883ca1da fix: cache refresh
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m10s
2026-03-23 16:29:11 +01:00
d357328e48 Merge pull request 'fix: add log in getAllClubArray' (#121) from dev into master
Reviewed-on: #121
2026-03-23 15:06:51 +00:00
2501b998fa fix: add log in getAllClubArray
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m8s
2026-03-23 16:05:09 +01:00
f9e4b83ad3 Merge pull request 'hot fix: remove save score button 2' (#120) from dev into master
Reviewed-on: #120
2026-03-21 11:09:17 +00:00
a7c89dbd73 hot fix: remove save score button 2
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m55s
2026-03-21 12:08:45 +01:00
ae087084e9 Merge pull request 'hot fix: remove save score button' (#119) from dev into master
Reviewed-on: #119
2026-03-21 11:00:12 +00:00
b2ee06d3b9 hot fix: remove save score button
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 44s
2026-03-21 11:59:37 +01:00
5afef2bcab Merge pull request 'fix: change obs lice name location' (#118) from dev into master
Reviewed-on: #118
2026-03-20 15:40:37 +00:00
3ed44bc044 fix: change obs lice name location
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m57s
2026-03-20 16:40:16 +01:00
55d39e02d2 Merge pull request 'fix: clear pub aff when no cat select' (#117) from dev into master
Reviewed-on: #117
2026-03-20 15:11:17 +00:00
d091503f19 fix: clear pub aff when no cat select
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m3s
2026-03-20 16:10:59 +01:00
d5c1273e18 Merge pull request 'fix: change methode for move upload file' (#116) from dev into master
Reviewed-on: #116
2026-03-18 12:07:58 +00:00
ea17989a82 fix: change methode for move upload file
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m59s
2026-03-18 13:07:39 +01:00
eda96d5ed2 Merge pull request 'fix: add more log for file copy' (#115) from dev into master
Reviewed-on: #115
2026-03-18 10:07:00 +00:00
e8555f7992 fix: add more log for file copy
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m0s
2026-03-18 11:06:40 +01:00
3a4e2f160f Merge pull request 'fix: add log for file copy' (#114) from dev into master
Reviewed-on: #114
2026-03-18 09:48:37 +00:00
fdd42c6a63 fix: add log for file copy
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m1s
2026-03-18 10:48:09 +01:00
2b84854ef2 Merge pull request 'fix: obs support wss' (#113) from dev into master
Reviewed-on: #113
2026-03-17 22:16:29 +00:00
ddf530ce4c fix: obs support wss
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m8s
2026-03-17 23:16:06 +01:00
fc3bea7f76 Merge pull request 'fix: language detection' (#112) from dev into master
Reviewed-on: #112
2026-03-16 11:19:50 +00:00
286911cf89 fix: language detection
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m2s
2026-03-16 12:19:30 +01:00
e67e01640a Merge pull request 'fix: club guest mixing in result' (#111) from dev into master
Reviewed-on: #111
2026-03-10 17:47:33 +00:00
4bfa6e845d fix: club guest mixing in result
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m3s
2026-03-10 13:59:35 +01:00
e870ffdda0 Merge pull request 'fix: eslint version' (#110) from dev into master
Reviewed-on: #110
2026-03-09 12:19:27 +00:00
806ead5b98 fix: eslint version
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m29s
2026-03-09 13:19:12 +01:00
378a17ba75 Merge pull request 'dev' (#109) from dev into master
Reviewed-on: #109
2026-03-09 12:10:46 +00:00
ee0a7d87e9 feat: cm Add audio encodeur generator
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 34s
2026-03-09 13:07:51 +01:00
812d873d5d fix: Match reorder refresh 2026-03-08 21:58:57 +01:00
d6d9f86254 feat: PDF for classement club 2026-02-27 11:10:31 +01:00
a22225ee7a feat: classement club 2026-02-23 12:01:36 +01:00
3cb12826d0 Merge pull request 'dev' (#108) from dev into master
Reviewed-on: #108
2026-02-20 16:59:40 +00:00
2f390b03e2 feat: add podium PDF
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m28s
2026-02-19 15:55:46 +01:00
ed5d73c25f feat: improve generation PDF for categories 2026-02-17 21:35:29 +01:00
0a368454c4 fix: duplicate id liceInput1 2026-02-16 22:49:49 +01:00
752f03cba5 feat: PDF for categories 2026-02-16 22:43:14 +01:00
d857fce71f feat: support decimal on weight 2026-02-13 21:42:48 +01:00
2fd09af0ea feat: auto categories full competition 2026-02-13 21:33:19 +01:00
d43cdc1a4e feat: auto categories 2026-02-11 14:08:13 +01:00
8663aa61cf feat: add silver cup png to cm 2026-02-06 18:02:11 +01:00
758e02dc5b feat: remove ^/api$ path log 2026-02-06 17:26:33 +01:00
72b248d09d Merge pull request 'dev' (#107) from dev into master
Reviewed-on: #107
2026-02-06 16:18:39 +00:00
31315c951a feat: auto poule
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m28s
2026-02-06 17:17:44 +01:00
ab4d57985c fix: add classement array in competition.js 2026-02-06 13:23:09 +01:00
2e846bf801 feat: use preset in cmt 2026-02-06 13:15:18 +01:00
9954dd002c feat: add config for preset in category + filter on select comb 2026-02-05 23:53:49 +01:00
7dab5b8880 fix: tree detection on cat edit 2026-02-05 22:32:31 +01:00
b37902466c feat: add team card when comb is register 2026-02-05 22:07:14 +01:00
89d9e04a6f feat: add classement match system 2026-02-05 21:29:27 +01:00
e2197d0712 fix: score panel fonction update 2026-02-01 15:25:49 +01:00
34a6911fa0 Merge pull request 'dev' (#106) from dev into master
Reviewed-on: #106
2026-01-30 14:07:08 +00:00
172dcdaa67 fix: ghost selection in PayAndValidateList
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m59s
2026-01-30 15:00:32 +01:00
cdd7221e86 fix: add weight config for all register source 2026-01-30 14:54:44 +01:00
952300d063 feat: card in result view 2026-01-30 14:50:21 +01:00
541d3824f3 feat: update CardModel 2026-01-30 11:58:31 +01:00
4c260b86b9 feat: remove old card system 2026-01-29 22:43:59 +01:00
9018ecef12 feat: card for team 2026-01-29 22:35:54 +01:00
d749dea6f4 feat: add return of table state 2026-01-27 22:11:49 +01:00
189eb135bb wip: competition card system 2026-01-26 22:36:26 +01:00
59ba31ae2d Merge pull request 'dev' (#105) from dev into master
Reviewed-on: #105
2026-01-23 15:49:24 +00:00
197ee0d5b1 feat: register import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m49s
2026-01-23 16:48:09 +01:00
3d8597869d feat: update register export 2026-01-22 19:04:08 +01:00
4a07eb4ed9 feat: comp register optional weight config 2026-01-22 15:13:48 +01:00
d2a7e6cbac feat: rework CatPreset for add more detail + add preset 2026-01-21 22:05:58 +01:00
78d22b466d feat: add lock picto and hide team on register admin + show registration information on user page 2026-01-20 22:53:47 +01:00
b419e2f58d feat: add category registration system to club and user 2026-01-20 21:49:10 +01:00
a5d3973394 feat: add category registration system to admin 2026-01-20 20:21:34 +01:00
4dff0940c1 feat: add docker healthcheck for api 2026-01-18 21:10:20 +01:00
4b56aa0209 Merge pull request 'dev' (#104) from dev into master
Reviewed-on: #104
2026-01-18 15:19:12 +00:00
eed7a10cfb feat: select comb double clic support and add background to already select
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m43s
2026-01-18 15:26:16 +01:00
1cbbde6506 feat: add team support to result 2026-01-18 15:10:38 +01:00
e8d5d0fa0c feat: add team support to cm 2026-01-17 22:37:48 +01:00
eefa77369a Merge pull request 'feat: update js npm lib' (#103) from dev into master
Reviewed-on: #103
2026-01-16 15:21:11 +00:00
a5bbc41dfd feat: update js npm lib
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m49s
2026-01-16 16:20:51 +01:00
a942798a6c Merge pull request 'feat: add match eq support' (#102) from dev into master
Reviewed-on: #102
2026-01-16 14:37:55 +00:00
01436ac220 feat: add match eq support
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m42s
2026-01-16 15:37:23 +01:00
876545630a Merge pull request 'fix: translate backend asset includes in native' (#101) from dev into master
Reviewed-on: #101
2026-01-16 13:22:09 +00:00
0150c4fac2 fix: translate backend asset includes in native
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m36s
2026-01-16 14:21:46 +01:00
cad6d14ba8 Merge pull request 'fix: translate backend add log' (#100) from dev into master
Reviewed-on: #100
2026-01-16 12:59:33 +00:00
c456fdae8b fix: translate backend add log
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m48s
2026-01-16 13:59:12 +01:00
4bf07cc8f8 Merge pull request 'i18n' (#99) from i18n into master
Reviewed-on: #99
2026-01-16 12:18:53 +00:00
a8356c8663 feat: translate backend
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m23s
2026-01-15 16:32:43 +01:00
c7a2215103 feat: translation competition manager part 2026-01-15 14:08:57 +01:00
46f543abde feat: translation competition part 2026-01-14 18:24:26 +01:00
d8b6f1ad25 feat: translation result 2026-01-14 15:51:15 +01:00
9170157dbb feat: init translation 2026-01-13 21:31:05 +01:00
babbc5d585 Merge pull request 'feat: keep showLicenceState in sessionStorage' (#98) from dev into master
Reviewed-on: #98
2026-01-07 21:10:12 +00:00
51c774a21e feat: keep showLicenceState in sessionStorage
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m52s
2026-01-07 22:09:41 +01:00
9d85b9ae45 Merge pull request 'feat: support ha partial config' (#97) from dev into master
Reviewed-on: #97
2026-01-07 15:35:10 +00:00
0ab0731ebd feat: support ha partial config
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m52s
2026-01-07 16:34:57 +01:00
ec48f73d9f Merge pull request 'feat: support ha partial config' (#96) from dev into master
Reviewed-on: #96
2026-01-07 15:24:44 +00:00
f98f1bd1ac feat: support ha partial config
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m59s
2026-01-07 16:24:19 +01:00
b9d752ac55 Merge pull request 'dev' (#95) from dev into master
Reviewed-on: #95
2026-01-05 16:57:01 +00:00
13650b27c1 feat: add selection
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m50s
2026-01-05 17:56:37 +01:00
0757ae7198 feat: allow admin and club admin to get comp result 2026-01-05 14:20:22 +01:00
a871b52006 feat: use blason setting 2026-01-05 11:09:22 +01:00
78959fc485 feat: cma select comb sort by name 2026-01-04 21:29:42 +01:00
0a91e72c29 feat: autorise null mail for admin add 2026-01-04 21:05:20 +01:00
e3a1d1c50b Merge pull request 'dev' (#94) from dev into master
Reviewed-on: #94
2026-01-03 20:50:18 +00:00
3933954e09 feat: add permission setting to admin page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m44s
2026-01-03 21:49:36 +01:00
959e356fb9 fix: quick add auto complet 2026-01-03 11:22:04 +01:00
3b5955ff31 fix: rm match with cardboard 2026-01-02 23:44:53 +01:00
e8ee5811a0 feat: avoid ws reconnect when user logged out 2026-01-02 23:12:22 +01:00
12326fe122 Merge pull request 'fix: ws timeout' (#93) from dev into master
Reviewed-on: #93
2026-01-02 21:33:40 +00:00
9624ff93f0 fix: ws timeout
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m42s
2026-01-02 22:33:11 +01:00
5444235512 Merge pull request 'dev' (#92) from dev into master
Reviewed-on: #92
2026-01-02 15:00:08 +00:00
b54eaa2549 fix: remove case sensitive sort result
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m44s
2026-01-02 15:58:59 +01:00
6a7af8aea9 fix: tree order on result api 2026-01-02 15:33:56 +01:00
bc4dbb532f fix: remove extra log on recalculateMatch 2026-01-02 15:13:41 +01:00
0e269e0bf7 Merge pull request 'dev' (#91) from dev into master
Reviewed-on: #91
2026-01-02 14:06:09 +00:00
6d6324e1b4 fix: result on empty category
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m45s
2026-01-02 15:05:30 +01:00
309f2a2268 feat: recalculateMatch add tmp log 2026-01-02 15:03:09 +01:00
13bae3e817 Merge pull request 'dev' (#90) from dev into master
Reviewed-on: #90
2026-01-02 13:42:36 +00:00
c6751b0428 fix: recalculateMatch 2026-01-02 14:42:02 +01:00
237952f95a fix: tree order on cmt 2026-01-02 14:29:11 +01:00
075f01a205 feat: add privacy setting in me page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m53s
2026-01-02 14:09:55 +01:00
82231744af feat: improve result page design 2026-01-02 13:48:36 +01:00
71a4d98434 fix: club remove 2026-01-02 13:47:54 +01:00
29a1184994 feat: add Result Privacy membre setting 2026-01-01 19:43:25 +01:00
3d07a5bc8e Merge pull request 'fix: rm licence' (#89) from dev into master
Reviewed-on: #89
2025-12-31 15:47:55 +00:00
a730384d37 fix: rm licence
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m42s
2025-12-31 16:47:37 +01:00
412e661319 Merge pull request 'fix: cat graphe color and text alignement' (#88) from dev into master
Reviewed-on: #88
2025-12-31 13:50:57 +00:00
bc72b7912c fix: cat graphe color and text alignement
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m46s
2025-12-31 14:50:15 +01:00
6359e7e027 Merge pull request 'fix: ci cache' (#87) from dev into master
Reviewed-on: #87
2025-12-31 13:15:42 +00:00
c7a9cc221f fix: ci cache
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m31s
2025-12-31 14:15:25 +01:00
17c61be171 Merge pull request 'fix: ci cache' (#86) from dev into master
Reviewed-on: #86
2025-12-31 13:12:17 +00:00
76f69c2154 fix: ci cache
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 1m30s
2025-12-31 14:11:43 +01:00
639f07ccdc Merge pull request 'fix: ci cache' (#85) from dev into master
Reviewed-on: #85
2025-12-31 13:07:57 +00:00
c02e37862c fix: ci cache
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2s
2025-12-31 14:07:37 +01:00
2b6dc48880 Merge pull request 'dev' (#84) from dev into master
Reviewed-on: #84
2025-12-31 12:59:50 +00:00
6879d9195d fix: remove useless log
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 7s
2025-12-31 13:59:33 +01:00
1595fb57c3 update: config + add action cache 2025-12-31 13:50:28 +01:00
8067d496f8 Merge pull request 'Re-update to 3.30.5' (#83) from dev into master
Reviewed-on: #83
2025-12-31 12:20:08 +00:00
aba8d8a202 Re-update to 3.30.5
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 8m10s
2025-12-31 13:19:31 +01:00
38663d97c2 Merge pull request 'Restore pom.xml' (#82) from dev into master
Reviewed-on: #82
2025-12-29 22:47:27 +00:00
377ce70125 Restore pom.xml
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m36s
2025-12-29 23:47:06 +01:00
61cee85335 Merge pull request 'Restore Quarkus 3.16.4' (#81) from dev into master
Reviewed-on: #81
2025-12-29 22:36:34 +00:00
19949d1e35 Restore Quarkus 3.16.4
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2m35s
2025-12-29 23:36:02 +01:00
03cdcf78c8 Merge pull request 'Move to Quarkus 3.20.4' (#80) from dev into master
Reviewed-on: #80
2025-12-29 22:30:24 +00:00
0b4f843084 Move to Quarkus 3.20.4
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2m20s
2025-12-29 23:30:01 +01:00
fa00273c87 Merge pull request 'Move to Quarkus 3.27.1' (#79) from dev into master
Reviewed-on: #79
2025-12-29 22:24:03 +00:00
b2438e52fc Move to Quarkus 3.27.1
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 5m4s
2025-12-29 23:23:42 +01:00
31bbe5719f Merge pull request 'dev' (#78) from dev into master
Reviewed-on: #78
2025-12-29 20:54:30 +00:00
bf377c9c5e feat: external website result connector
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2m31s
2025-12-29 21:03:05 +01:00
145d5e7ca8 feat: update quarkus 2025-12-29 15:41:13 +01:00
448e49b9fd feat: add obs connector 2025-12-28 21:22:28 +01:00
b7a1e58436 fix: cma comb dupli on poule list 2025-12-27 17:34:58 +01:00
a73a3562c3 feat: add scrolle to match list in cmt 2025-12-27 17:29:28 +01:00
0dace76fc3 feat: cat remove 2025-12-27 17:06:27 +01:00
f76ca43ed6 feat: add import fail log detail 2025-12-27 16:55:23 +01:00
c6b24a1b20 feat: add fname formatter 2025-12-27 16:43:14 +01:00
c47c04c29e fix: cat update/add refresh 2025-12-27 15:00:50 +01:00
e06df24002 ty-fix: ws timeout 2025-12-27 14:20:10 +01:00
352c34064f fix: score save update 2025-12-27 14:18:38 +01:00
2ccd2a9178 fix: disable match filter on 1 zone cat + rename lice to zone 2025-12-27 13:02:45 +01:00
0ff3ffd66b Merge pull request 'feat: add club_tresorier to club admin' (#77) from dev into master
Reviewed-on: #77
2025-12-26 21:19:10 +00:00
847674d62c feat: add club_tresorier to club admin
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m46s
2025-12-26 22:18:03 +01:00
8896e2a28a Merge pull request 'fix: delect club without user' (#76) from dev into master
Reviewed-on: #76
2025-12-26 13:37:05 +00:00
68b4f8e476 fix: delect club without user
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m40s
2025-12-26 14:35:19 +01:00
d3fb884ab4 Merge pull request 'fix: Me page' (#75) from dev into master
Reviewed-on: #75
2025-12-26 13:16:56 +00:00
81faafe2de fix: Me page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m46s
2025-12-26 14:16:30 +01:00
ee730be570 Merge pull request 'dev' (#74) from dev into master
Reviewed-on: #74
2025-12-25 19:03:17 +00:00
4856326b29 feat: html mail for all public
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 11m4s
2025-12-25 19:59:47 +01:00
b94f4b43ce fix: reword message + fix missing comb un cat list (cma) 2025-12-25 19:27:15 +01:00
795d044c0b fix:typo 2025-12-25 19:25:56 +01:00
dc5ab9c3c4 feat: add inactif member filter 2025-12-20 18:22:24 +01:00
870891a77c Merge pull request 'dev' (#73) from dev into master
Reviewed-on: #73
2025-12-19 15:14:04 +00:00
7e1356b621 fix: cm auto ask new category on empty competition
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m17s
2025-12-19 16:13:37 +01:00
b689521e56 fix: comb register with null license 2025-12-19 16:02:28 +01:00
e81f475402 Merge pull request 'dev-comp' (#72) from dev into master
Reviewed-on: #72
2025-12-19 13:47:40 +00:00
095a5fcede Merge branch 'dev-comp' into dev
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m20s
2025-12-19 14:43:46 +01:00
73f026210c feat: cm home page 2025-12-19 14:37:24 +01:00
4b969e6d69 feat: add CardPanel 2025-12-19 00:30:35 +01:00
4706af27f8 feat: add tooltip for menu 2025-12-18 19:19:25 +01:00
3e8c19534b feat: add score panel 2025-12-18 19:00:39 +01:00
3decc6d0e0 Merge pull request 'dev' (#71) from dev into master
Reviewed-on: #71
2025-12-18 15:37:19 +00:00
254fd7d73f feat: html email
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m5s
2025-12-18 16:36:30 +01:00
12a75d003c feat: mask inactive club on map 2025-12-18 15:33:45 +01:00
7f957b98f2 fix: deleteLicence Panache missing transaction context 2025-12-18 15:24:32 +01:00
8b2c9613fd Merge pull request 'dev' (#70) from dev into master
Reviewed-on: #70
2025-12-18 14:03:51 +00:00
50b49c40e3 fix: error 500 when license is null
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m6s
2025-12-18 15:03:22 +01:00
47d3921e05 fix: email typo 2025-12-18 14:57:26 +01:00
ecc9753237 Merge pull request 'feat: add cat filter on mass license validation' (#69) from dev into master
Reviewed-on: #69
2025-12-18 13:47:32 +00:00
7c4addd525 feat: add cat filter on mass license validation
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m27s
2025-12-18 14:46:52 +01:00
a1b5ca2694 fix: pubAdd chrono init 2025-12-17 21:00:43 +01:00
c5f7b81ac3 feat: move to MatchPanel 2025-12-17 20:54:04 +01:00
7f999733dc feat: chrono 2025-12-17 20:43:49 +01:00
0ac92fcda3 feat: add public screen 2025-12-15 23:08:46 +01:00
8e01fb46f8 feat: SmartLogoBackground 2025-12-15 21:00:19 +01:00
09a51edd5f wip: public screen 2025-12-10 00:08:14 +01:00
354fbfede9 feat: add flag svg 2025-12-09 22:49:25 +01:00
ef5b707697 feat: cm table next tree match + lock scores 2025-12-07 19:45:09 +01:00
489bfeb354 feat: cm table matches & scores 2025-12-07 15:12:48 +01:00
a8565534e6 update README.md 2025-12-05 15:28:03 +01:00
bb901392fc feat: cm admin poule editor 2025-12-02 15:58:20 +01:00
6f61db6817 feat: cm admin poule selector 2025-11-30 21:52:05 +01:00
00701fb874 feat: cm admin comb selector 2025-11-30 19:16:32 +01:00
936392f8bd feat: add guest comb to competition register form 2025-11-30 18:38:18 +01:00
f6d4bb0fe4 wip: cm admin comb selector 2025-11-26 12:37:32 +01:00
160c7d59e3 fix: move data model to good package 2025-11-25 18:43:40 +01:00
9689201a8c feat: add anti-blink timer 2025-11-25 17:24:21 +01:00
083d72fbfa feat: update Bootstrap 2025-11-25 17:18:25 +01:00
1c7466d883 fix: categorie perm 2025-11-25 17:17:48 +01:00
c9c8c8536d feat: add categorie info in CM 2025-11-24 19:28:04 +01:00
b1bcf75e56 feat: categorie on CM 2025-11-24 15:56:15 +01:00
b78b3f005b feat: implement ws 2025-11-21 14:06:46 +01:00
a83088387b wip: implement ws base 2025-11-20 23:39:02 +01:00
22fa896ee0 feat: competition result 2025-11-19 17:53:11 +01:00
f8976deb91 feat: re-add siret/rna api 2025-11-19 16:47:14 +01:00
eb9badb4a1 fix: error on loading logo on maps 2025-11-19 16:47:14 +01:00
661fcdb16b fix: remove siret/rna api 2025-11-19 16:47:14 +01:00
b2ad633b21 feat: membre list filter history 2025-11-19 16:47:13 +01:00
1b5bf8ba6c feat: add categorie filter membre search 2025-11-19 16:47:13 +01:00
bf75d9d036 feat: make user-friendly cat name 2025-11-19 16:47:13 +01:00
9457c5749a feat: add ordering for member page 2025-11-19 16:47:13 +01:00
c7f56881cd feat: upgrade club groupe name in kc when club is rename 2025-11-19 16:47:13 +01:00
aebcd62aa9 feat: upgrade aff find to user all id (Rna, siret, siren) 2025-11-19 16:47:13 +01:00
c2eecf4906 fix: certifDate NaN all membre export 2025-11-19 16:47:13 +01:00
87b3bc12e0 fix: null licence on import 2025-11-19 16:47:13 +01:00
3d3d63e58c fix: add more log to import 2025-11-19 16:47:13 +01:00
81c115c655 fix: empty mail on import 2025-11-19 16:47:13 +01:00
a7ba1d16a4 feat: remove kc new account mail 2025-11-19 16:47:13 +01:00
645949a2f6 feat: add cache to getAssoInfo 2025-11-19 16:47:13 +01:00
beb40db1b1 feat: merge rna and siret fields 2025-11-19 16:47:13 +01:00
61a4af6ff1 fix: club delete on RegisterModel 2025-11-19 16:47:13 +01:00
d9fc68298c feat: allow empty mail 2025-11-19 16:47:13 +01:00
fccea5bf6a fix: aff renew select length 2025-11-19 16:47:13 +01:00
ee476cd0e2 feat: remove saison selection on aff req 2025-11-19 16:47:13 +01:00
b320d7db37 fix: affiliation ok login msg 2025-11-19 16:47:13 +01:00
80fef98e07 feat: add email tooltip 2025-11-19 16:47:13 +01:00
7e80703c04 feat: add re-login message 2025-11-19 16:47:10 +01:00
cc4a3e4e06 fix: null email on import 2025-11-19 16:46:46 +01:00
9e28356f2c fix: log detail 2025-11-19 16:46:46 +01:00
77d66813c7 fix: log detail 2025-11-19 16:46:46 +01:00
7625da1d4b feat: add aff req log detail
fix: membre import null email filter
2025-11-19 16:46:46 +01:00
4262845074 fix: typo 2025-11-19 16:46:46 +01:00
b84e10de44 feat: keep log 2025-11-19 16:46:46 +01:00
ac6563ac95 fix: club order 2025-11-19 16:46:46 +01:00
baf57c3464 fix: log message length 2025-11-19 16:46:46 +01:00
43fcbb5ef6 Merge pull request 'feat: re-add siret/rna api' (#68) from dev into master
Reviewed-on: #68
2025-11-19 14:31:46 +00:00
fefc5d651b feat: re-add siret/rna api
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m4s
2025-11-19 15:26:36 +01:00
163d9b23e9 Merge pull request 'fix: error on loading logo on maps' (#67) from dev into master
Reviewed-on: #67
2025-11-18 21:02:58 +00:00
53509820c6 fix: error on loading logo on maps
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m58s
2025-11-18 22:02:13 +01:00
a8ad3a7558 Merge pull request 'fix: remove siret/rna api' (#66) from dev into master
Reviewed-on: #66
2025-11-18 20:46:34 +00:00
f46e268d39 fix: remove siret/rna api
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 1m33s
2025-11-18 21:46:05 +01:00
abad3194a4 Merge pull request 'dev' (#65) from dev into master
Reviewed-on: #65
2025-11-18 19:32:14 +00:00
6e7eec0587 feat: membre list filter history
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m16s
2025-11-18 15:45:18 +01:00
8c83668364 feat: add categorie filter membre search 2025-11-18 14:02:02 +01:00
771c06ccd8 feat: make user-friendly cat name 2025-11-18 13:27:49 +01:00
b479b992cf feat: add ordering for member page 2025-11-17 21:48:24 +01:00
f018e52afa Merge pull request 'dev' (#64) from dev into master
Reviewed-on: #64
2025-11-14 15:55:53 +00:00
be2f01c070 feat: upgrade club groupe name in kc when club is rename
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m13s
2025-11-14 16:38:44 +01:00
9ab50238b9 feat: upgrade aff find to user all id (Rna, siret, siren) 2025-11-14 16:17:40 +01:00
cf5d93630f fix: certifDate NaN all membre export 2025-11-14 14:46:58 +01:00
e5e17d3862 Merge pull request 'fix: null licence on import' (#63) from dev into master
Reviewed-on: #63
2025-11-14 13:32:09 +00:00
7410569ced fix: null licence on import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m3s
2025-11-14 14:31:37 +01:00
b107f443aa Merge pull request 'fix: add more log to import' (#62) from dev into master
Reviewed-on: #62
2025-11-14 13:15:27 +00:00
8e2d68ebd5 fix: add more log to import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m10s
2025-11-14 14:14:22 +01:00
f050127fd7 Merge pull request 'dev' (#61) from dev into master
Reviewed-on: #61
2025-11-14 12:45:53 +00:00
d02fd63834 fix: empty mail on import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m30s
2025-11-14 13:43:51 +01:00
b143cc759f feat: remove kc new account mail 2025-11-12 16:38:35 +01:00
cc5534ef00 feat: add cache to getAssoInfo 2025-11-12 16:15:40 +01:00
26f56006f6 feat: merge rna and siret fields 2025-11-12 14:56:28 +01:00
f8dacee3e7 Merge pull request 'fix: club delete on RegisterModel' (#60) from dev into master
Reviewed-on: #60
2025-11-08 18:09:04 +00:00
ed1f30f2b6 fix: club delete on RegisterModel
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m17s
2025-11-08 19:08:41 +01:00
8517e9824c Merge pull request 'feat: allow empty mail' (#59) from dev into master
Reviewed-on: #59
2025-11-08 17:45:02 +00:00
6cec8ff31d feat: allow empty mail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m16s
2025-11-08 18:44:35 +01:00
e7deba52e9 Merge pull request 'dev' (#58) from dev into master
Reviewed-on: #58
2025-11-07 15:31:18 +00:00
d95c173fa8 fix: aff renew select length
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m51s
2025-11-07 16:19:49 +01:00
8a0e4423f2 feat: remove saison selection on aff req 2025-11-07 16:13:37 +01:00
b956236934 fix: affiliation ok login msg 2025-11-07 15:57:36 +01:00
7767c98304 feat: add email tooltip 2025-11-07 15:55:10 +01:00
7e380ccb69 feat: add re-login message 2025-11-07 15:31:25 +01:00
94d1148eb1 fix: null email on import 2025-11-07 15:30:56 +01:00
8a14f58ce5 Merge pull request 'fix: log detail' (#57) from dev into master
Reviewed-on: #57
2025-11-05 21:06:54 +00:00
79dbbdaaec fix: log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m27s
2025-11-05 22:06:36 +01:00
bf5704db54 Merge pull request 'fix: log detail' (#56) from dev into master
Reviewed-on: #56
2025-11-05 20:43:26 +00:00
dec98f9508 fix: log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m22s
2025-11-05 21:43:05 +01:00
d3a62e980d Merge pull request 'feat: add aff req log detail' (#55) from dev into master
Reviewed-on: #55
2025-11-05 20:19:10 +00:00
b89ed62795 feat: add aff req log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m33s
fix: membre import null email filter
2025-11-05 21:18:29 +01:00
5ffc9fb495 Merge pull request 'fix: typo' (#54) from dev into master
Reviewed-on: #54
2025-09-05 18:31:45 +00:00
5e48bc4623 fix: typo
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m51s
2025-09-05 20:31:10 +02:00
81b953fb05 Merge pull request 'dev' (#53) from dev into master
Reviewed-on: #53
2025-09-03 19:47:13 +00:00
c6659f8d85 feat: keep log
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m19s
2025-09-03 21:46:26 +02:00
e86fe42b3d fix: club order 2025-09-03 21:35:36 +02:00
ef528aa524 fix: log message length 2025-09-03 19:53:19 +02:00
d1c7f37a94 feat: competition result 2025-09-03 18:56:58 +02:00
e6cc4cbc96 Merge pull request 'dev' (#52) from dev into master
Reviewed-on: #52
2025-08-21 09:43:54 +00:00
587173c79f feat: exel export register data
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m16s
2025-08-21 11:37:03 +02:00
0fc871bd46 feat: ban competition + competition list sort 2025-08-21 11:11:04 +02:00
2a1bdfbdcb feat: helloasso competition register 2025-08-20 22:38:05 +02:00
11dca5630c feat: register for club and free mode
wip: helloasso
2025-08-20 16:48:11 +02:00
c58dedf80a Merge pull request 'competition rework' (#51) from dev into master
Reviewed-on: #51
2025-08-18 09:33:51 +00:00
dedae02676 feat: rework competition perm, naming, competition data
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m6s
2025-08-17 22:07:12 +02:00
9e9391465d feat: update competition perm 2025-08-16 11:14:41 +02:00
1908de681e Merge pull request 'dev' (#50) from dev into master
Reviewed-on: #50
2025-08-15 19:49:13 +00:00
18ea38f85a fix: address build + aff req admin bug
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m0s
2025-08-15 21:48:16 +02:00
d740ad255f fix: affiliation update email membre 2025-08-15 21:26:38 +02:00
76381c75bd Merge pull request 'fix: Membre edition' (#49) from dev into master
Reviewed-on: #49
2025-08-15 18:34:43 +00:00
49bb471b60 fix: Membre edition
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m4s
2025-08-15 20:34:22 +02:00
7d281196c1 Merge pull request 'dev' (#48) from dev into master
Reviewed-on: #48
2025-08-15 16:26:23 +00:00
fee96e7900 Merge branch 'master' into dev
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m24s
2025-08-15 16:26:02 +00:00
1e37c43dcd feat: add checkout table auto clean + disable pay button for pre deployment test 2025-08-15 18:25:05 +02:00
15f65b1014 feat: HelloAsso payment 2025-08-15 17:44:12 +02:00
0a56f8c180 feat: add licence log 2025-08-13 22:29:49 +02:00
0563c7c8de feat: add pay information to licence 2025-08-13 22:05:19 +02:00
09f6cd7463 feat: masse licence validation 2025-08-13 15:32:06 +02:00
7bd5e7baa5 feat: check for email duplication when create or update membre 2025-08-13 15:16:39 +02:00
f75e805cc0 feat: update welcome mail 2025-08-13 14:58:25 +02:00
1cd4a1ff97 feat: licence state filter membreList 2025-08-12 19:13:58 +02:00
41a88ea914 feat: lock membre creation with same name 2025-08-12 15:52:19 +02:00
c85c28fee2 feat: lock full name change on membre edit 2025-08-12 15:39:36 +02:00
580104de00 feat:check email in import + move pdf code in service 2025-08-12 15:30:28 +02:00
d145fc1b2e Merge pull request 'update: maps update api url' (#47) from dev into master
Reviewed-on: #47
2025-07-07 19:18:49 +00:00
3a788ba9b3 update: maps update api url
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m48s
2025-07-07 21:17:18 +02:00
c032ae81cd Merge pull request 'feat: update cat' (#46) from dev into master
Reviewed-on: #46
2025-07-07 17:24:09 +00:00
cd2cc5e345 feat: update cat
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m50s
2025-07-07 19:23:55 +02:00
169beb9637 Merge pull request 'update cat' (#45) from dev into master
Reviewed-on: #45
2025-07-07 14:06:33 +00:00
337b1ab0fd wip: update cat
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m0s
2025-07-07 16:06:03 +02:00
a138af013e Merge pull request 'dev' (#44) from dev into master
Reviewed-on: #44
2025-07-07 13:33:41 +00:00
f1d7be2a4c fix: licence import endpoint
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m50s
2025-07-07 15:32:46 +02:00
f51a00f62a fix: licence not full color 2025-07-07 14:14:08 +02:00
cda2c397c4 Merge pull request 'feat: add licence import endpoint' (#43) from dev into master
Reviewed-on: #43
2025-07-07 12:06:07 +00:00
824bedb99f feat: add licence import endpoint
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m50s
fix: pdf_gen
2025-07-07 14:05:22 +02:00
c8511db01c Merge pull request 'dev' (#42) from dev into master
Reviewed-on: #42
2025-07-07 11:25:34 +00:00
bd59a17212 feat: add affiliation pdf generator
Some checks failed
Deploy Production Server / if_merged (pull_request) Has been cancelled
2025-07-07 13:24:44 +02:00
34be19fbed feat: add affiliation pdf generator 2025-07-07 13:16:54 +02:00
ee6d41c155 Merge pull request 'dev' (#41) from dev into master
Reviewed-on: #41
2025-07-05 19:22:59 +00:00
280a8fa3f3 Merge branch 'master' into dev
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 13m18s
2025-07-05 19:22:35 +00:00
9705182547 feat: add club_id to LicenceModel for stats 2025-07-05 21:19:57 +02:00
3fd5ae3741 feat: admin club filter fetch all club
fix: log null update case
2025-07-05 14:11:05 +02:00
1e260caddb feat: add club contact tooltip 2025-07-05 13:42:16 +02:00
520d00adc7 feat: add new affiliationRequest mail notif 2025-07-04 17:46:30 +02:00
dea91714fa feat: add reject reason for affiliationRequest 2025-07-04 17:24:51 +02:00
97ca8766af wip: Logger 2025-07-04 16:52:59 +02:00
54e72cc705 feat: refacto update membre
wip: Logger
2025-07-03 17:25:09 +02:00
ad58958f49 wip: LoggerService 2025-07-02 23:07:01 +02:00
0c4b88b5cd feat: add licence and user remove condition check 2025-07-02 19:02:46 +02:00
5ce5df950f feat: club membre import 2025-07-02 18:47:30 +02:00
17fea2272f feat: club membre export 2025-07-01 16:38:49 +02:00
de62510f45 Merge pull request 'fix(cat): age map' (#40) from dev into master
Reviewed-on: #40
2025-03-17 11:13:43 +01:00
e18ed652f0 fix(cat): age map
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m55s
2025-03-17 11:12:56 +01:00
6f24d7d5bf Merge pull request 'dev' (#39) from dev into master
Reviewed-on: #39
2025-03-15 17:46:46 +01:00
7497cc399c fix(comb): category calculator
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m56s
2025-03-15 17:44:41 +01:00
ee77975c9c fix(db): rm mysql + better name research 2025-03-15 17:44:22 +01:00
ae0c0041c9 Merge pull request 'fix(register): get by name set condition to AND' (#38) from dev into master
Reviewed-on: #38
2025-03-15 11:21:57 +01:00
2cdc7e4aac fix(register): get by name set condition to AND
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m18s
2025-03-15 09:00:42 +01:00
7a574f4531 Merge pull request 'fix(register): set club on new' (#37) from dev into master
Reviewed-on: #37
2025-03-15 01:00:06 +01:00
2db82d19f3 fix(register): set club on new
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m17s
2025-03-15 00:58:20 +01:00
678cf4f76a Merge pull request 'dev' (#36) from dev into master
Reviewed-on: #36
2025-03-15 00:19:03 +01:00
fa17d0a037 feat(comp): add more register data
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m25s
2025-03-13 16:27:15 +01:00
c7b5f3ef61 fix(comp): delete poule 2025-03-11 11:35:08 +01:00
bda1a6ef2a Merge pull request 'dev' (#35) from dev into master
Reviewed-on: #35
2025-03-11 11:16:18 +01:00
fbbfef37ba feat(comp): check perm for creation
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 8m22s
2025-03-11 11:14:26 +01:00
15c88c4943 fix(comp): rm permission and empty poule list on rm 2025-03-11 10:52:24 +01:00
df58e93886 Merge pull request 'dev' (#34) from dev into master
Reviewed-on: #34
2025-02-14 16:07:14 +01:00
42711cde5d feat(aff): add internal contact field to aff request
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m36s
2025-02-14 16:03:45 +01:00
5d80f20999 feat(maps): add link to contact 2025-02-14 15:38:21 +01:00
9e9e8525d6 feat(club): add other contact type for club + fix switching presentation order when change type 2025-02-14 14:44:54 +01:00
9f4adbe4a4 fix(club): remove clubId field 2025-02-14 11:32:21 +01:00
26e79126f5 fix(membre): remove not dev national team feature 2025-02-14 11:29:41 +01:00
8504508396 fix(stat): remove useless hide text on graph 2025-02-14 11:22:47 +01:00
f3fe80fa46 feat(insc): add safca connector 2025-02-13 20:32:53 +01:00
743a6911f5 feat(insc): add frontend 2025-02-12 14:24:27 +01:00
3f1fddccd1 feat(insc): add frontend 2025-02-11 21:23:03 +01:00
bf39a53aeb Merge pull request 'dev' (#33) from dev into master
Reviewed-on: #33
2025-02-10 13:01:58 +01:00
df33d49cff fix(comb): no licence on Safca connector
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m18s
2025-02-10 13:00:26 +01:00
ccdb9e17fe update(docker) 2025-02-10 11:57:09 +01:00
e2e50ae3db feat(stat): add stats page 2025-02-09 15:48:29 +01:00
0769e5ae9d feat(me): add password rest button 2025-02-08 19:10:21 +01:00
2cdb495402 Merge pull request 'dev' (#32) from dev into master
Reviewed-on: #32
2025-02-08 14:01:04 +01:00
e3fd3878b2 fix(club): status file extension
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m12s
2025-02-08 13:39:36 +01:00
0ff2c399b3 fix(aff_req): fix npe on licences 2025-02-08 13:14:55 +01:00
f5db6b7178 Merge pull request 'fix(aff_req): default value' (#31) from dev into master
Reviewed-on: #31
2025-02-07 13:15:23 +01:00
2411ebced7 fix(aff_req): default value
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m12s
2025-02-07 13:13:25 +01:00
85fa81e3d9 Merge pull request 'fix(membre): fix null licence number case' (#30) from dev into master
Reviewed-on: #30
2025-02-07 12:53:59 +01:00
4bcb72ebec fix(membre): fix null licence number case
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m10s
2025-02-07 12:52:34 +01:00
7269a8f3ab Merge pull request 'fix(swagger): enable in production' (#29) from dev into master
Reviewed-on: #29
2025-02-07 12:10:01 +01:00
d5a0b8f554 fix(swagger): enable in production
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m15s
2025-02-07 12:08:20 +01:00
b8cf1c76c1 Merge pull request 'fix(aff_req): no club membre fail loading' (#28) from dev into master
Reviewed-on: #28
2025-02-07 11:43:09 +01:00
dac1fd77c5 fix(aff_req): no club membre fail loading
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m17s
2025-02-07 11:41:39 +01:00
677cfb955d Merge pull request 'dev' (#27) from dev into master
Reviewed-on: #27
2025-02-06 14:31:44 +01:00
d613439a82 fix(licence): admin role null contexte
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m10s
2025-02-06 14:29:55 +01:00
1b39c438bf fix(error): update error mapper 2025-02-06 14:28:39 +01:00
4e9686d6a7 fix(search): add case insensitive 2025-02-06 13:57:31 +01:00
b983f4e3ec fix(pdf): no photo 2025-02-06 13:43:38 +01:00
4b430e241f fix(build): docker build 2025-02-06 11:43:49 +01:00
a21356239f Merge pull request 'fix(build): create sub-app make_pdf' (#26) from dev into master
Reviewed-on: #26
2025-02-06 11:17:41 +01:00
1585bee150 fix(build): create sub-app make_pdf
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 6m41s
2025-02-06 11:16:15 +01:00
9ecd5c4939 Merge pull request 'fix(build): create sub-app make_pdf' (#25) from dev into master
Reviewed-on: #25
2025-02-06 11:14:30 +01:00
4c83b80b6d fix(build): create sub-app make_pdf
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 18s
2025-02-06 11:12:33 +01:00
53566db2a3 Merge pull request 'fix(build): update reflect-config.json' (#24) from dev into master
Reviewed-on: #24
2025-02-05 17:43:40 +01:00
9f8d9dd0ca fix(build): update reflect-config.json
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m16s
2025-02-05 17:42:10 +01:00
d7263ae3f2 Merge pull request 'fix(build): update reflect-config.json' (#23) from dev into master
Reviewed-on: #23
2025-02-05 17:33:21 +01:00
ed23aca9c8 fix(build): update reflect-config.json
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m43s
2025-02-05 17:31:54 +01:00
2be70722db Merge pull request 'fix(build): update reflect-config.json' (#22) from dev into master
Reviewed-on: #22
2025-02-05 17:24:43 +01:00
840c005fcc fix(build): update reflect-config.json
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m49s
2025-02-05 17:23:17 +01:00
ac5b68dff1 Merge pull request 'fix(build): add reflect-config.json' (#21) from dev into master
Reviewed-on: #21
2025-02-05 17:14:10 +01:00
da80fc5efa fix(build): add reflect-config.json
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m30s
2025-02-05 17:12:45 +01:00
5e67820f7c Merge pull request 'dev' (#20) from dev into master
Reviewed-on: #20
2025-02-05 15:47:28 +01:00
f2cda5aa34 fix(pdf): use quarkus version of openpdf
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m42s
2025-02-05 15:45:37 +01:00
b79455fb03 fix(club): add club status access to club_respo_intra role 2025-02-05 11:46:38 +01:00
7085473c00 fix(admin_membre): error loading licence on null certificate 2025-02-05 11:43:11 +01:00
3869ad50e9 fix(me): access to photo 2025-02-05 11:38:18 +01:00
972d599642 fix(aff): lazy loading on licence in validation aff req 2025-02-05 11:21:46 +01:00
7d35472ea5 fix(aff): licence number parsing 2025-02-05 11:12:43 +01:00
87f7a717f3 Merge pull request 'wip(utils): mime type fix' (#19) from dev into master
Reviewed-on: #19
2025-02-04 17:08:01 +01:00
f5dbe0b6af wip(utils): mime type fix
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m2s
2025-02-04 17:06:04 +01:00
a8182d4cf9 Merge pull request 'wip(utils): mime type fix' (#18) from dev into master
Reviewed-on: #18
2025-02-04 16:30:46 +01:00
1ccbd92c34 Merge branch 'master' into dev
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 5m22s
2025-02-04 16:30:16 +01:00
c598f4fe21 wip(utils): mime type fix 2025-02-04 16:29:06 +01:00
34348293b4 wip(utils): mime type fix
All checks were successful
Deploy Production Server / if_merged (pull_request) Has been skipped
2025-02-04 16:24:01 +01:00
6b86a4f30d Merge pull request 'dev' (#17) from dev into master
Reviewed-on: #17
2025-02-04 16:11:17 +01:00
071b35f2d4 wip(utils): mime type fix
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m24s
2025-02-04 16:09:21 +01:00
eb68f84f7c fix(aff): set auto role after affiliation accepte 2025-01-22 21:48:39 +01:00
254fd0582f fix(membre): null licence number on creation 2025-01-22 21:15:46 +01:00
c2d602b9bf fix: remove double affiliation on new club 2025-01-22 21:04:39 +01:00
098e98b719 fix: access to personal pp 2025-01-22 20:44:18 +01:00
893c5ef456 Merge pull request 'fix: re-enable email kc feature' (#16) from dev into master
Reviewed-on: #16
2025-01-22 20:23:28 +01:00
22d742ab63 fix: re-enable email kc feature
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 4m54s
2025-01-22 20:22:10 +01:00
e291e5b050 Merge pull request 'fix: try fix email' (#15) from dev into master
Reviewed-on: #15
2025-01-22 19:11:41 +01:00
ec09fdc2cf fix: try fix email
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 5m1s
2025-01-22 19:10:15 +01:00
d59038fbc1 Merge pull request 'fix: email sender name' (#14) from dev into master
Reviewed-on: #14
2025-01-22 11:39:58 +01:00
5cb36cb3c0 fix: email sender name
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 5m0s
2025-01-22 11:38:35 +01:00
da33a221e9 Merge pull request 'feat: add next redirection system' (#13) from dev into master
Reviewed-on: #13
2025-01-22 11:21:36 +01:00
fb8396b588 feat: add next redirection system
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 5m2s
2025-01-22 11:19:56 +01:00
67d2fa633f feat: update quarkus
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 5m29s
2025-01-21 23:15:11 +01:00
c344299cdb feat(mail): add affiliation accept message
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 5m12s
2025-01-21 20:35:39 +01:00
24effa1f78 feat(mail): add account notification mail 2025-01-21 15:10:14 +01:00
ccf287f482 feat(add): send check email on change 2025-01-21 11:34:48 +01:00
b3540e84db fix(account): kc send mail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 4m48s
2025-01-21 11:18:51 +01:00
c20ea7cedc feat(account): add email.enabled setting
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 4m45s
2025-01-21 11:01:54 +01:00
a489ddedc3 fix(kc): Leave all club groupe when setClubGroupMembre is call whit null ClubModel 2025-01-21 10:55:56 +01:00
81f256109a fix(club): null affiliation id on club list page 2025-01-20 20:46:20 +01:00
a0d31688ac fix(db): Column type of ClubModel
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 4m38s
2025-01-20 19:19:50 +01:00
6e7dd94d03 fix(club): NPE on features
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 4m53s
2025-01-20 15:56:55 +01:00
8ecb9d714f update(map): public club map
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 5m26s
2025-01-20 15:40:09 +01:00
476f2b5233 fix: location research 2025-01-20 15:21:54 +01:00
52973d189f feat: add club map 2025-01-20 14:53:42 +01:00
0f96554ce2 feat: set home page 2025-01-19 13:21:21 +01:00
b4cb29c258 wip: set home page 2025-01-18 13:29:49 +01:00
dd90a829a9 wip: set home page 2025-01-17 22:58:41 +01:00
520c3ad89b feat: change site name and icon 2025-01-17 11:22:44 +01:00
8623275854 feat(membre): add certificat make date 2025-01-17 10:56:46 +01:00
f637300e87 fix(build): add asset resource tout native exec 2025-01-17 10:10:02 +01:00
fefd5c5080 fix(utils): remove JMI for MimeType 2025-01-10 16:22:14 +01:00
3258a8a12c fix(utils): remove JMI for MimeType
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 4m48s
2025-01-10 16:01:27 +01:00
173f7ececa fix(utils): remove JMI for MimeType
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 5m16s
2025-01-10 15:48:51 +01:00
a73c5d0ecb update: ci/cd 2025-01-08 20:32:51 +01:00
58b84c2451 wip: ci/cd
Some checks failed
Deploy Production Server / if_merged (push) Failing after 49s
2024-12-29 13:21:56 +01:00
3c8721d706 Merge pull request 'dev' (#1) from dev into master
Reviewed-on: https://git.poupouche.fr/Thibaut/ffsaf-site/pulls/1
2024-12-29 13:15:18 +01:00
4519f5437f Merge branch 'master' into dev
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 44s
2024-12-29 13:14:58 +01:00
b17abd776e update: ci/cd 2024-12-29 13:13:12 +01:00
aac126cb87 feat: pdf generation 2024-12-29 11:34:39 +01:00
6b38405e94 feat: secure match ans poule endpoints 2024-08-15 13:56:54 +02:00
bd386d1b0a feat: add SecurityContext class 2024-08-15 11:44:54 +02:00
7f80c876d3 wip: send competition data 2024-08-14 21:33:11 +02:00
4be7b28efd feat: move competition setting to ffsaf site 2024-08-09 12:12:29 +02:00
27dd22080c feat: move competition setting to ffsaf site 2024-08-09 12:09:37 +02:00
b766525000 fix: last-name to upper case 2024-08-03 17:12:03 +02:00
a57492ac19 Merge remote-tracking branch 'origin/dev' into dev 2024-07-20 09:13:09 +02:00
cc00da4e5e feat: swagger 2024-07-20 09:12:50 +02:00
b3bfb7e267 wip: swagger p3 2024-07-19 23:30:38 +02:00
fa54a58394 wip: swagger p2 2024-07-19 19:29:30 +02:00
957fcfff8b wip: swagger 2024-07-19 17:21:11 +02:00
3ccf880057 feat: change certif type 2024-07-19 15:25:54 +02:00
bc73c40be1 feat: change certif type 2024-07-19 15:24:58 +02:00
47daa459e2 feat: better error message 2024-07-19 14:52:05 +02:00
a58dcdd08e feat: mePage 2024-07-18 21:40:36 +02:00
8ba3f45215 feat: add flags 2024-07-18 18:05:38 +02:00
53d59a5b56 feat: club set role 2024-07-18 11:46:54 +02:00
fe1af4d78f Merge remote-tracking branch 'origin/dev' into dev 2024-07-18 10:29:43 +02:00
b93a08da71 feat: club page
wip: edit af request
2024-07-18 10:29:16 +02:00
c7a2133eed feat: club page
wip: edit af request
2024-07-17 23:11:59 +02:00
43f7a54b15 fix: club set role 2024-07-17 12:21:37 +02:00
5ba4ee1f90 feat: membre required field light 2024-07-17 11:46:13 +02:00
f84ad91dc8 feat: licence add number 2024-07-17 11:14:26 +02:00
d268461bfd feat: club 2024-07-17 10:54:42 +02:00
edcda185db feat: club filter 2024-07-16 23:16:46 +02:00
f297ae557b feat: club remove 2024-07-16 22:08:02 +02:00
6407bf44bc fix: affiliation request page minor bug 2024-07-16 14:24:52 +02:00
2737e53de5 feat: affiliation request list 2024-07-16 13:41:03 +02:00
dae32e3607 feat: affiliation request 2024-07-16 12:45:11 +02:00
d03ec054d2 wip: affiliation 2024-07-15 23:39:01 +02:00
682894f326 wip: Affiliation request 2024-07-14 23:04:22 +02:00
b2438ec3d8 feat: AffiliationCard 2024-07-14 15:00:28 +02:00
6a21bd4735 feat: club end main 2024-07-14 13:26:41 +02:00
e1a8c90f3e wip: club end main 2024-07-13 22:42:04 +02:00
1b74c0a3bd wip: club add map 2024-07-13 18:34:18 +02:00
6c4b01590d wip: addr field 2024-07-12 16:37:04 +02:00
76d7a28678 wip: club 2024-07-11 23:02:29 +02:00
TitiOnFire
0c9f1ccb0d Merge pull request #25
dev
2024-03-22 20:44:00 +01:00
d5b4820c79 feat: send record v2 2024-03-22 20:43:39 +01:00
3c0f1b01d6 wip: send record v2 2024-03-22 17:32:17 +01:00
TitiOnFire
2a9fc01f81 Merge pull request #24
dev
2024-03-21 16:34:13 +01:00
9b5192c395 fix: deploy timeout command 2024-03-21 16:33:29 +01:00
fd0f4f59ab fix: save media dir 2024-03-21 16:32:31 +01:00
TitiOnFire
d5734245ac Merge pull request #22
dev
2024-03-21 15:10:02 +01:00
4cb426a478 feat: receive match record cancel and timeout 2024-03-21 15:08:35 +01:00
3211c642f5 feat: receive match record 2024-03-19 21:27:43 +01:00
TitiOnFire
b2db1e7e05 Merge pull request #21
dev
2024-03-10 21:51:31 +01:00
f7c33044d7 fix: membre research 2024-03-10 21:51:12 +01:00
TitiOnFire
20faaba30b Merge pull request #20
dev
2024-03-10 21:11:09 +01:00
7fd17f0223 fix: membre research 2024-03-10 21:10:47 +01:00
TitiOnFire
eb2d22bfe1 Merge pull request #19
dev
2024-03-08 19:11:10 +01:00
cdca5c1aab feat: send mail at account creation 2024-03-08 19:10:29 +01:00
TitiOnFire
d7882b5bff Merge pull request #18
dev
2024-03-08 16:31:34 +01:00
bd757fc731 wip: test ws 2024-03-08 16:30:08 +01:00
bbc8c470c5 feat: add membre 2024-03-06 19:57:17 +01:00
9615660101 feat: add affiliation request save 2024-03-06 15:17:20 +01:00
b0232cd7b7 feat: add membre 2024-02-28 12:07:24 +01:00
TitiOnFire
b3d9b1df83 Merge pull request #17 from TitiOnFire/dev
Dev
2024-02-09 19:47:15 +01:00
2fd6ef3c2e feat: add membre list filter 2024-02-06 21:39:42 +01:00
40427b8cfb feat: start affiliation system 2024-02-05 21:42:50 +01:00
2a59c22db6 feat: add club member's gestion page 2024-02-04 22:13:21 +01:00
0c9020890a feat: add licence admin gestion 2024-02-04 13:49:01 +01:00
c6130cf65f feat: add data model for licence 2024-02-03 19:23:37 +01:00
58c2134e35 feat: add set userId 2024-02-03 19:05:33 +01:00
TitiOnFire
e92e75f371 Merge pull request #16
dev
2024-02-03 14:37:12 +01:00
TitiOnFire
9d50dbcfa4 Merge pull request #15
fix keycloak interaction reflexion
2024-01-30 20:24:36 +01:00
TitiOnFire
72b78d7f4a Merge pull request #14
add keycloak interaction
2024-01-30 20:07:42 +01:00
TitiOnFire
9923c22522 Merge pull request #13
dev
2024-01-25 18:33:53 +01:00
TitiOnFire
52afe12a01 Merge pull request #12
dev
2024-01-25 18:10:51 +01:00
TitiOnFire
fec95f5335 Merge pull request #11 from TitiOnFire/dev
remove quinoa
2024-01-24 23:34:17 +01:00
TitiOnFire
6cff5eec07 Merge pull request #10 from TitiOnFire/dev
update deploy script
2024-01-24 23:31:46 +01:00
TitiOnFire
96697df25a Merge pull request #9 from TitiOnFire/dev
update deploy script
2024-01-24 23:25:22 +01:00
TitiOnFire
077086621b Merge pull request #8 from TitiOnFire/dev
fix router
2024-01-24 23:16:38 +01:00
TitiOnFire
78a55dc96d Merge pull request #7 from TitiOnFire/dev
update build
2024-01-24 13:14:51 +01:00
TitiOnFire
258b5ff590 Merge pull request #6 from TitiOnFire/dev
test
2024-01-23 23:15:08 +01:00
TitiOnFire
93f7396669 Merge pull request #5 from TitiOnFire/dev
test
2024-01-23 23:03:53 +01:00
TitiOnFire
8cc03de96d Merge pull request #4 from TitiOnFire/dev
Dev
2024-01-23 22:56:55 +01:00
TitiOnFire
b844cd45c1 Merge pull request #3 from TitiOnFire/dev
remove vite proxy
2024-01-14 21:21:36 +01:00
TitiOnFire
cfc9ae376d Merge pull request #2
dev
2024-01-14 20:47:43 +01:00
TitiOnFire
18c198ac9c Merge pull request #1 from TitiOnFire/dev
test auto-deploy
2024-01-14 18:49:12 +01:00
844 changed files with 41938 additions and 2715 deletions

View File

@@ -1,5 +1,5 @@
*
!target/*-runner
!target/*-runner.jar
!target/lib/*
!target/quarkus-app/*
postgres-data/
ffsaf-media/
docker-compose.yml
ffsaf_cle_prive.jks
prod.env

View File

@@ -0,0 +1,118 @@
name: Deploy Production Server
# Only run the workflow when a PR is merged on main and closed
on:
pull_request:
types:
- closed
branches:
- 'master'
# Here we check that the PR was correctly merged to main
jobs:
if_merged:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache node modules
id: cache-npm
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'graalvm'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: src/main/webapp/package-lock.json
- name: Build frontend
run: |
echo "${{ vars.VITE_ENV }}" > src/main/webapp/.env
cd src/main/webapp
npm install
npm run build
cd ../../..
- name: Inject frontend in backend
run: |
rm -rf src/main/resources/META-INF/resources
mkdir -p src/main/resources/META-INF/
mv src/main/webapp/dist src/main/resources/META-INF/resources
- name: Build backend make_pdf tool
run: |
chmod 740 mvnw
cd src/main/pdf_gen
../../../mvnw clean compile assembly:single
cd ../../..
- name: Build backend
run: |
chmod 740 mvnw
./mvnw package -Pnative -DskipTests
- name: Copy runner to vps via scp
uses: appleboy/scp-action@v0.1.7 # Latest in date when creating the workflow
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_KEY }}
source: "target/*-runner,src/main/resources/cacerts,src/main/docker/Dockerfile.native,docker-compose.yml,.dockerignore,src/main/pdf_gen/target/pdf_gen-*.jar"
target: ${{ secrets.TARGET_DIR }} # Need to create it first on the VPS
- name: Re-start ffsaf container
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_KEY }}
script: |
cd ${{ secrets.TARGET_DIR }}
docker logs ffsaf > "log/ffsaf_logs_$(date +"%Y-%m-%d_%H-%M-%S").log" 2>&1
docker stop ffsaf
docker rm ffsaf
docker compose up --build -d ffsaf
- name: Check ffsaf container
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_KEY }}
script: |
if docker ps | grep ffsaf; then
echo 'Container is running'
else
echo 'Container is not running'
exit 1 # This mark the pipeline as failed
fi

View File

@@ -59,6 +59,7 @@ jobs:
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_KEY }}
command_timeout: 20m
script: |
cd ${{ secrets.TARGET_DIR }}/ffsaf
cp ../vite.env src/main/webapp/.env

4
.gitignore vendored
View File

@@ -45,5 +45,9 @@ nb-configuration.xml
# Custom
/config/application.properties
/cle_prive.jks
/mail-truststore.p12
/src/main/resources/META-INF/resources/
/media/
/media-ext/
/sign.jpg
/.gitea/workflows/test.yml

View File

@@ -1,8 +1,22 @@
# ffsaf-site
# FFSAF - Intranet
This project uses Quarkus, the Supersonic Subatomic Java Framework.
## Introduction et context
If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ .
Lintranet de la Fédération française de Soft Armored Fighting a pour but centralise la prise de licences des adhérents
et laffiliation des clubs. Il permet aussi de recueillir les résultats des compétitions organisées par les clubs et,
plus récemment, den faciliter lorganisation grâce à un outil intégré.
Le système de prise de licence se divise en trois parties :
* Un formulaire public pour la première demande daffiliation ;
* Un espace club, accessible après validation, pour la saisie des informations relatives aux demandes de licence des
adhérents ;
* Un espace fédération pour accepter les demandes de licences et daffiliation.
Un espace membre permet enfin à chaque adhérent de télécharger son attestation de licence.
Pour les compétitions, le système permet la création de catégories, de poules et de matchs, avec génération automatique
des matchs au sein dune poule. Il supporte plusieurs lices, assure la synchronisation en temps réel des modifications
entre toutes les instances de lapplication web et publie automatiquement les résultats sur le site de lorganisateur.
## Running the application in dev mode
@@ -52,34 +66,3 @@ Or, if you don't have GraalVM installed, you can run the native executable build
You can then execute your native executable with: `./target/ffsaf-site-1.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.
## Related Guides
- Reactive MySQL client ([guide](https://quarkus.io/guides/reactive-sql-clients)): Connect to the MySQL database using the reactive pattern
- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A Jakarta REST implementation utilizing build time processing and Vert.x.
This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.
- Hibernate ORM with Panache ([guide](https://quarkus.io/guides/hibernate-orm-panache)): Simplify your persistence code for Hibernate ORM via the
active record or the repository pattern
- Reactive PostgreSQL client ([guide](https://quarkus.io/guides/reactive-sql-clients)): Connect to the PostgreSQL database using the reactive pattern
## Provided Code
### Hibernate ORM
Create your first JPA entity
[Related guide section...](https://quarkus.io/guides/hibernate-orm)
[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache)
### RESTEasy Reactive
Easily start your Reactive RESTful Web Services
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources)
### RESTEasy Reactive Qute
Create your web page using Quarkus RESTEasy Reactive & Qute
[Related guide section...](https://quarkus.io/guides/qute#type-safe-templates)

56
docker-compose.yml Normal file
View File

@@ -0,0 +1,56 @@
services:
ffsaf:
container_name: ffsaf
hostname: ffsaf
restart: always
build:
context: .
dockerfile: src/main/docker/Dockerfile.native
volumes:
- ${PWD}/ffsaf.properties:/work/config/application.properties
- ${PWD}/ffsaf_cle_prive.jks:/work/cle_prive.jks
- ${PWD}/mail-truststore.p12:/work/mail-truststore.p12
- ${PWD}/sign.jpg:/work/sign.jpg
- ${PWD}/ffsaf-media:/work/media
depends_on:
ffsaf-db:
condition: service_healthy
restart: true
networks:
- default
- intra
- nginx
healthcheck:
test: [ "CMD", "curl", "-f", "https://intra.ffsaf.fr/api" ]
interval: 30s
timeout: 5s
retries: 3
ffsaf-db:
image: public.ecr.aws/docker/library/postgres:17.2
hostname: ffsaf-db
container_name: ffsaf-db
user: postgres
restart: always
networks:
- pgadmin
- default
healthcheck:
test: [ "CMD-SHELL", "pg_isready" ]
interval: 5s
timeout: 3s
retries: 10
volumes:
- ${PWD}/postgres-data:/var/lib/postgresql/data
env_file: prod.env
networks:
intra:
name: intra
driver: bridge
pgadmin:
name: pgadmin
external: true
nginx:
name: ${NETWORK_NAME:-gateway}
external: true

83
pom.xml
View File

@@ -1,20 +1,19 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.titionfire</groupId>
<artifactId>ffsaf-site</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<compiler-plugin.version>3.11.0</compiler-plugin.version>
<compiler-plugin.version>3.12.1</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.6.5</quarkus.platform.version>
<quarkus.platform.version>3.30.5</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.1.2</surefire-plugin.version>
<surefire-plugin.version>3.2.3</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -34,49 +33,52 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-qute</artifactId>
<artifactId>quarkus-rest-qute</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-reactive-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-mysql-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mssql-client</artifactId>
<version>4.4.1</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.antivirus</groupId>
<artifactId>quarkus-antivirus</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-authorization</artifactId>
@@ -84,14 +86,13 @@
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-admin-client-reactive</artifactId>
<artifactId>quarkus-keycloak-admin-rest-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<version>1.18.42</version>
<scope>provided</scope>
</dependency>
@@ -100,6 +101,39 @@
<artifactId>jodd-util</artifactId>
<version>6.2.1</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>fop</artifactId>
<version>2.11</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
@@ -168,10 +202,11 @@
</activation>
<properties>
<skipITs>false</skipITs>
<quarkus.package.type>native</quarkus.package.type>
<quarkus.native.additional-build-args>
-H:+UnlockExperimentalVMOptions
--initialize-at-run-time=com.fasterxml.jackson.databind.ext.DOMDeserializer
</quarkus.native.additional-build-args>
<quarkus.native.enabled>true</quarkus.native.enabled>
<quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>
</properties>
</profile>
</profiles>

View File

@@ -14,13 +14,17 @@
# docker run -i --rm -p 8080:8080 quarkus/ffsaf-site
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9
# replace FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 for jvm need sub application (ie. make_pdf)
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
USER 0
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root ffsaf/target/*-runner /work/application
COPY --chown=1001:root ffsaf/src/main/resources/cacerts /work/cacerts
COPY --chown=1001:root target/*-runner /work/application
COPY --chown=1001:root src/main/resources/cacerts /work/cacerts
COPY --chown=1001:root src/main/pdf_gen/target/pdf_gen-*.jar /work/make_pdf.jar
RUN mkdir /work/media && chown -R 1001:root /work/media
EXPOSE 8080

View File

@@ -2,12 +2,14 @@ package fr.titionfire;
import io.quarkus.oidc.IdToken;
import io.quarkus.oidc.RefreshToken;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.jboss.resteasy.reactive.NoCache;
@Path("/hello")
@@ -30,6 +32,9 @@ public class ExampleResource {
@IdToken
JsonWebToken idToken;
@Inject
UserInfo userInfo;
/**
* Injection point for the Access Token issued by the OpenID Connect Provider
*/
@@ -54,12 +59,14 @@ public class ExampleResource {
@GET
@Produces("text/html")
@NoCache
@Operation(hidden = true)
public String getTokens() {
StringBuilder response = new StringBuilder().append("<html>")
.append("<body>")
.append("<ul>");
System.out.println(idToken);
System.out.println(accessToken);
Object userName = this.idToken.getClaim("preferred_username");
if (userName != null) {
@@ -69,20 +76,17 @@ public class ExampleResource {
response.append("<li>username: ").append(this.idToken.toString()).append("</li>");
}
Object scopes = this.accessToken.getClaim("scope");
/*Object scopes = this.accessToken.getClaim("scope");
if (scopes != null) {
response.append("<li>scopes: ").append(scopes.toString()).append("</li>");
}
if (scopes != null) {
response.append("<li>scopes: ").append(this.accessToken.toString()).append("</li>");
}
response.append("<li>scopes: ").append(this.accessToken.getClaim("user_groups").toString()).append("</li>");*/
if (scopes != null) {
response.append("<li>getRoles: ").append(this.securityIdentity.getRoles()).append("</li>");
}
response.append("<li>refresh_token: ").append(refreshToken.getToken() != null).append("</li>");
return response.append("</ul>").append("</body>").append("</html>").toString();

View File

@@ -0,0 +1,27 @@
package fr.titionfire;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
@Tag(name = "Ping API", description = "API pour tester la connectivité")
@Path("/api")
public class PingPage {
@Operation(summary = "Renvoie un message de réussite", description = "Cette méthode renvoie un message de réussite si la connexion est établie avec succès.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite")
})
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response get() {
return Response.ok().build();
}
}

View File

@@ -1,17 +1,16 @@
package fr.titionfire;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import static java.util.Objects.requireNonNull;
@Path("/some-page")
@Path("api/some-page")
public class SomePage {
private final Template page;
@@ -22,8 +21,12 @@ public class SomePage {
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get(@QueryParam("name") String name) {
return page.data("name", name);
@Operation(hidden = true)
public Uni<String> get() {
return Uni.createFrom()
.completionStage(() -> page
.data("name", "test")
.renderAsync());
}
}

View File

@@ -44,7 +44,9 @@ public class FrontendForwardingFilter implements ContainerResponseFilter {
final String path = info.getPath();
final String address = request.remoteAddress().toString();
if (!path.equals("/api")) {
LOG.infof("Request %s %s from IP %s", method, path, address);
}
int status = responseContext.getStatus();
if (status != 404 && !(status == 405 && "GET".equals(requestContext.getMethod()))) {

View File

@@ -0,0 +1,68 @@
package fr.titionfire.ffsaf;
import io.vertx.core.http.HttpServerRequest;
import jakarta.annotation.Priority;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Scanner;
@Provider
@Priority(Priorities.USER)
public class FrontendForwardingFilterREST implements ExceptionMapper<NotFoundException> {
private static final Logger LOG = Logger.getLogger(FrontendForwardingFilterREST.class);
@Context
UriInfo info;
@Context
HttpServerRequest request;
@Context
ResourceInfo resourceInfo;
private static String text = null;
private static final String API_NAMESPACE_REGEX = "^/(api/.*|api|openapi|q/.*)";
private static final String FILENAME_REGEX = "^/.*\\.[^.]+$";
@Override
public Response toResponse(NotFoundException exception) {
final String path = info.getPath();
final String address = request.remoteAddress().toString();
boolean isApiNamespace = path.matches(API_NAMESPACE_REGEX);
if (isApiNamespace) {
LOG.infof("Request %s from IP %s => %d", "method", path, address, exception.getResponse().getStatus());
return exception.getResponse();
}
boolean isFilename = path.matches(FILENAME_REGEX);
if (isFilename) {
LOG.infof("Request %s from IP %s => %d", "method", path, address, exception.getResponse().getStatus());
return exception.getResponse();
}
boolean actualErrorResponse = resourceInfo != null && resourceInfo.getResourceMethod() != null;
if (actualErrorResponse) {
LOG.infof("Request %s from IP %s => %d", "method", path, address, exception.getResponse().getStatus());
return exception.getResponse();
}
if (text == null)
text = new Scanner(
Objects.requireNonNull(this.getClass().getResourceAsStream("/META-INF/resources/index.html")),
StandardCharsets.UTF_8).useDelimiter("\\A").next();
LOG.infof("Request %s from IP %s => redirect", "method", path, address);
return Response.status(200).entity(text).type(MediaType.TEXT_HTML_TYPE).build();
}
}

View File

@@ -0,0 +1,48 @@
package fr.titionfire.ffsaf;
import fr.titionfire.ffsaf.domain.service.TradService;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.ext.Provider;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@Provider
@PreMatching
public class UserInfoProvider implements ContainerRequestFilter {
private static final List<Locale> SUPPORTED_LANGUAGES = Arrays.asList(
Locale.FRENCH,
Locale.ENGLISH
);
@Override
public void filter(ContainerRequestContext requestContext) {
List<Locale> acceptableLanguages = requestContext.getAcceptableLanguages();
Locale selectedLocale = findFirstSupportedLanguage(acceptableLanguages);
if (selectedLocale == null)
selectedLocale = TradService.fallbackLocale;
requestContext.setProperty("userLocale", selectedLocale);
}
private Locale findFirstSupportedLanguage(List<Locale> acceptableLanguages) {
for (Locale acceptableLanguage : acceptableLanguages) {
// Vérifie si la langue est dans la liste des langues supportées
if (SUPPORTED_LANGUAGES.contains(acceptableLanguage)) {
return acceptableLanguage;
}
// Vérifie aussi par tag de langue (ex: "fr-FR" -> "fr")
String languageTag = acceptableLanguage.getLanguage();
for (Locale supportedLanguage : SUPPORTED_LANGUAGES) {
if (supportedLanguage.getLanguage().equals(languageTag)) {
return supportedLanguage;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,35 @@
package fr.titionfire.ffsaf.data;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.dialect.PostgreSQLDialect;
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
import org.hibernate.type.BasicType;
import org.hibernate.type.BasicTypeRegistry;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.spi.TypeConfiguration;
import org.jboss.logging.Logger;
public class CustomPostgreSQLDialect extends PostgreSQLDialect {
private static final Logger LOGGER = Logger.getLogger(PostgreSQLDialect.class);
public CustomPostgreSQLDialect() {
super();
}
@Override
public void initializeFunctionRegistry(FunctionContributions functionContributions) {
super.initializeFunctionRegistry(functionContributions);
LOGGER.info("Initializing custom function registry");
SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry();
TypeConfiguration typeConfiguration = functionContributions.getTypeConfiguration();
BasicTypeRegistry basicTypeRegistry = typeConfiguration.getBasicTypeRegistry();
BasicType<String> stringType = basicTypeRegistry.resolve(StandardBasicTypes.STRING);
functionRegistry.namedDescriptorBuilder("unaccent").setInvariantType(stringType).setExactArgumentCount(1)
.setParameterTypes(new FunctionParameterType[]{FunctionParameterType.STRING}).register();
}
}

View File

@@ -0,0 +1,17 @@
package fr.titionfire.ffsaf.data.id;
import jakarta.persistence.Embeddable;
import lombok.*;
import java.io.Serializable;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Embeddable
public class RegisterId implements Serializable {
private Long competitionId;
private Long membreId;
}

View File

@@ -0,0 +1,29 @@
package fr.titionfire.ffsaf.data.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "affiliation")
public class AffiliationModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "Identifiant de l'affiliation", example = "42")
Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "club", referencedColumnName = "id")
ClubModel club;
@Schema(description = "Saison de l'affiliation", example = "2021")
int saison;
}

View File

@@ -0,0 +1,47 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.RoleAsso;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "affiliation_request")
public class AffiliationRequestModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
String state_id;
String address;
String contact;
String m1_lname;
String m1_fname;
String m1_email;
int m1_lincence;
RoleAsso m1_role;
String m2_lname;
String m2_fname;
String m2_email;
int m2_lincence;
RoleAsso m2_role;
String m3_lname;
String m3_fname;
String m3_email;
int m3_lincence;
RoleAsso m3_role;
int saison;
}

View File

@@ -0,0 +1,64 @@
package fr.titionfire.ffsaf.data.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import java.util.Date;
import java.util.Objects;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "card")
public class CardModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
Long comb;
Long match;
Long category;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "competition", referencedColumnName = "id")
CompetitionModel competition;
@JsonProperty("competition")
Long competitionId;
CardType type;
String reason;
@CreationTimestamp
Date date;
@Column(nullable = false, columnDefinition = "boolean default false")
boolean teamCard = false;
public boolean hasEffect(MatchModel match) {
return switch (this.type) {
case BLUE -> false;
case YELLOW -> Objects.equals(this.match, match.getId());
case RED -> Objects.equals(this.category, match.getCategory().getId())
|| Objects.equals(this.match, match.getId());
case BLACK -> true;
};
}
public enum CardType {
BLUE,
YELLOW,
RED,
BLACK
}
}

View File

@@ -0,0 +1,89 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "category_preset")
public class CatPresetModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "competition", referencedColumnName = "id")
CompetitionModel competition;
String name = "";
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "category_preset_catconfig", joinColumns = @JoinColumn(name = "id_preset"))
List<CategorieEmbeddable> categories;
SwordType swordType = SwordType.NONE;
ShieldType shieldType = ShieldType.NONE;
/*
* 1 - 1 - Casque
* 2 - 2 - Gorgerin
* 3 - 4 - Coquille et Protection pelvienne
* 4 - 8 - Gant main(s) armée(s)
* 5 - 16 - Gant main bouclier
* 6 - 32 - Plastron
* 7 - 64 - Protection de bras armé(s)
* 8 - 128 - Protection de bras de bouclier
* 9 - 256 - Protection de jambes
* 10 - 512 - Protection de genoux
* 11 - 1024 - Protection de coudes
* 12 - 2048 - Protection dorsale
* 13 - 4096 - Protection de pieds
*/
int mandatoryProtection1 = 0;
int mandatoryProtection2 = 0;
@ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY)
private List<RegisterModel> registers = new ArrayList<>();
@ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY)
private List<CompetitionGuestModel> guest = new ArrayList<>();
public enum SwordType {
NONE,
ONE_HAND,
TWO_HAND,
SABER
}
public enum ShieldType {
NONE,
STANDARD,
ROUND,
TEARDROP,
BUCKLER
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Embeddable
public static class CategorieEmbeddable {
Categorie categorie;
long roundDuration;
long pauseDuration;
}
}

View File

@@ -0,0 +1,57 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "category")
public class CategoryModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name = "system_type")
CompetitionSystem system;
Long systemId;
String name = "";
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "id_compet", referencedColumnName = "id")
CompetitionModel compet;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "id_category", referencedColumnName = "id")
List<MatchModel> matchs;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "id_category", referencedColumnName = "id")
List<TreeModel> tree;
Integer type;
String liceName = "1";
@Column(nullable = false, columnDefinition = "boolean default false")
boolean treeAreClassement = false;
@Column(nullable = false, columnDefinition = "boolean default false")
boolean fullClassement = false;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "id_preset", referencedColumnName = "id")
CatPresetModel preset;
}

View File

@@ -0,0 +1,41 @@
package fr.titionfire.ffsaf.data.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.util.Date;
import java.util.List;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "checkout")
public class CheckoutModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "Identifiant du checkout", example = "42")
Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membre", referencedColumnName = "id")
MembreModel membre;
Date creationDate = new Date();
List<Long> licenseIds;
Integer checkoutId;
PaymentStatus paymentStatus;
public enum PaymentStatus {
PENDING, AUTHORIZED, REFUSED, UNKNOW, REGISTERED, REFUNDING, REFUNDED, CONTESTED
}
}

View File

@@ -0,0 +1,38 @@
package fr.titionfire.ffsaf.data.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.util.Date;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "card_team")
public class ClubCardModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
Long competition;
String teamUuid;
String teamName;
CardModel.CardType type;
String reason;
@CreationTimestamp
Date date;
List<Long> cardIds;
}

View File

@@ -4,7 +4,9 @@ import fr.titionfire.ffsaf.utils.Contact;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.util.List;
import java.util.Map;
@Getter
@@ -16,37 +18,63 @@ import java.util.Map;
@Entity
@Table(name = "club")
public class ClubModel {
public class ClubModel implements LoggableModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "Identifiant du club", example = "1")
Long id;
@Schema(description = "Identifiant long du club (UUID)", example = "b94f3167-3f6a-449c-a73b-ec84202bf07e")
String clubId;
@Schema(description = "Nom du club", example = "Association sportive")
String name;
@Schema(description = "Pays du club", example = "FR")
String country;
String shieldURL;
//@Enumerated(EnumType.STRING)
@ElementCollection
@CollectionTable(name = "club_contact_mapping",
joinColumns = {@JoinColumn(name = "club_id", referencedColumnName = "id")})
@MapKeyColumn(name = "contact_type")
@Schema(description = "Les contacts du club", example = "{\"SITE\": \"www.test.com\", \"COURRIEL\": \"test@test.com\"}")
Map<Contact, String> contact;
@Column(columnDefinition = "TEXT")
@Schema(description = "Liste des lieux d'entraînement", example = "[{\"text\":\"addr 1\",\"lng\":2.24654,\"lat\":52.4868658},{\"text\":\"addr 2\",\"lng\":2.88654,\"lat\":52.7865456}]")
String training_location;
@Column(columnDefinition = "TEXT")
@Schema(description = "Liste des jours et horaires d'entraînement (jours 0-6, 0=>lundi) (temps en minute depuis 00:00, 122=>2h02)", example = "[{\"day\":0,\"time_start\":164,\"time_end\":240},{\"day\":3,\"time_start\":124,\"time_end\":250}]")
String training_day_time;
@Schema(description = "Contact interne du club", example = "john.doe@test.com")
String contact_intern;
String RNA;
@Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris")
String address;
String SIRET;
@Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234")
String StateId;
String no_affiliation;
@Schema(description = "Numéro d'affiliation du club", example = "12345")
Long no_affiliation;
@Schema(description = "Club international", example = "false")
boolean international;
@OneToMany(mappedBy = "club", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@Schema(description = "Liste des affiliations du club (optionnel)")
List<AffiliationModel> affiliations;
@Override
public String getObjectName() {
return this.name;
}
@Override
public LogModel.ObjectType getObjectType() {
return LogModel.ObjectType.Club;
}
}

View File

@@ -0,0 +1,11 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
public interface CombModel {
Long getCombId();
String getName();
String getName(MembreModel model, ResultPrivacy privacy);
Categorie getCategorie();
}

View File

@@ -0,0 +1,115 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "competition_guest")
public class CompetitionGuestModel implements CombModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "competition", referencedColumnName = "id")
CompetitionModel competition;
String lname = "";
String fname = "";
Categorie categorie = null;
String club = null;
Genre genre = null;
String country = "fr";
Float weight = null;
Float weightReal = null;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinTable(
name = "groupe_membre",
joinColumns = @JoinColumn(name = "groupe_id"),
inverseJoinColumns = @JoinColumn(name = "membre_id")
)
List<MembreModel> comb = new ArrayList<>();
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinTable(
name = "groupe_guest",
joinColumns = @JoinColumn(name = "groupe_id"),
inverseJoinColumns = @JoinColumn(name = "guest_id")
)
List<CompetitionGuestModel> guest = new ArrayList<>();
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinTable(
name = "categories_insc_guest",
joinColumns = @JoinColumn(name = "guest_id"),
inverseJoinColumns = @JoinColumn(name = "category_id")
)
List<CatPresetModel> categoriesInscrites = new ArrayList<>();
public CompetitionGuestModel(String s) {
this.fname = s.substring(0, s.indexOf(" "));
this.lname = s.substring(s.indexOf(" ") + 1);
}
@Override
public Long getCombId() {
return this.id * -1;
}
@Override
public String getName() {
if (this.isTeam())
return this.fname;
return this.fname + " " + this.lname;
}
@Override
public String getName(MembreModel model, ResultPrivacy privacy) {
return getName();
}
public boolean isTeam() {
return "__team".equals(this.lname);
}
public boolean isInTeam(Object comb_) {
if (!this.isTeam())
return false;
if (comb_ instanceof Long id_) {
if (id_ >= 0)
return comb.stream().anyMatch(membre -> Objects.equals(membre.getId(), id_));
else
return guest.stream().anyMatch(guestModel -> Objects.equals(guestModel.getId(), -id_));
}
return Stream.concat(comb.stream(), guest.stream()).anyMatch(c -> Objects.equals(c, comb_));
}
public Float getWeight2() {
return (this.weightReal != null) ? this.weightReal : this.weight;
}
}

View File

@@ -0,0 +1,86 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import fr.titionfire.ffsaf.utils.RegisterMode;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "compet")
public class CompetitionModel {
@Id
@Access(AccessType.PROPERTY)
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name = "system_type")
CompetitionSystem system;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "club", referencedColumnName = "id")
ClubModel club;
String name;
String uuid;
Date date;
Date todate;
@Column(columnDefinition = "TEXT")
String description;
String adresse;
Date startRegister;
Date endRegister;
RegisterMode registerMode;
boolean publicVisible;
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
List<RegisterModel> insc;
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
List<CompetitionGuestModel> guests = new ArrayList<>();
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
List<CatPresetModel> catPreset = new ArrayList<>();
List<Categorie> requiredWeight = new ArrayList<>();
List<Long> banMembre = new ArrayList<>();
String owner;
List<String> admin = new ArrayList<>();
@Column(name = "table_")
List<String> table = new ArrayList<>();
@Column(columnDefinition = "TEXT")
String data1;
@Column(columnDefinition = "TEXT")
String data2;
@Column(columnDefinition = "TEXT")
String data3;
@Column(columnDefinition = "TEXT")
String data4;
@Column(columnDefinition = "TEXT")
String config;
}

View File

@@ -0,0 +1,41 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.data.id.RegisterId;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "helloasso_register")
public class HelloAssoRegisterModel {
@EmbeddedId
RegisterId id;
@MapsId("competitionId")
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "id_competition")
CompetitionModel competition;
@MapsId("membreId")
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "id_membre")
MembreModel membre;
Integer orderId;
public HelloAssoRegisterModel(CompetitionModel competition, MembreModel membre, Integer orderId) {
this.id = new RegisterId(competition.getId(), membre.getId());
this.competition = competition;
this.membre = membre;
this.orderId = orderId;
}
}

View File

@@ -0,0 +1,52 @@
package fr.titionfire.ffsaf.data.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "licence")
public class LicenceModel implements LoggableModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "L'identifiant de la licence.")
Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membre", referencedColumnName = "id")
@Schema(description = "Le membre de la licence. (optionnel)")
MembreModel membre;
Long club_id;
@Schema(description = "La saison de la licence.", example = "2025")
int saison;
@Schema(description = "Nom et date du médecin sur certificat médical.", example = "M. Jean¤2025-02-03", format = "<Nom>¤<yyyy-mm-dd>")
String certificate;
@Schema(description = "Licence validée", example = "true")
boolean validate;
@Schema(description = "Licence payer", example = "true")
@Column(nullable = false, columnDefinition = "boolean default false")
boolean pay = false;
@Override
public String getObjectName() {
return "licence " + id.toString();
}
@Override
public LogModel.ObjectType getObjectType() {
return LogModel.ObjectType.Licence;
}
}

View File

@@ -0,0 +1,47 @@
package fr.titionfire.ffsaf.data.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import java.util.Date;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "log")
public class LogModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String subject;
Date dateTime;
ActionType action;
ObjectType object;
Long target_id;
@Column(columnDefinition = "TEXT")
String target_name;
@Column(columnDefinition = "TEXT")
String message;
public enum ActionType {
ADD, REMOVE, UPDATE
}
public enum ObjectType {
Membre, Affiliation, Licence, Club, Competition, Register, Selection
}
}

View File

@@ -0,0 +1,7 @@
package fr.titionfire.ffsaf.data.model;
public interface LoggableModel {
Long getId();
String getObjectName();
LogModel.ObjectType getObjectType();
}

View File

@@ -0,0 +1,153 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@ToString
@Table(name = "match")
public class MatchModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name = "system_type")
CompetitionSystem system;
Long systemId;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "c1", referencedColumnName = "id")
MembreModel c1_id = null;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "c1_guest", referencedColumnName = "id")
CompetitionGuestModel c1_guest = null;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "c2", referencedColumnName = "id")
MembreModel c2_id = null;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "c2_guest", referencedColumnName = "id")
CompetitionGuestModel c2_guest = null;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "id_category", referencedColumnName = "id")
CategoryModel category = null;
long category_ord = 0;
boolean isEnd = true;
Date date = null;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "score", joinColumns = @JoinColumn(name = "id_match"))
List<ScoreEmbeddable> scores = new ArrayList<>();
char poule = 'A';
public String getC1Name(MembreModel model, ResultPrivacy privacy) {
if (c1_id != null)
return c1_id.getName(model, privacy);
if (c1_guest != null)
return c1_guest.getName(model, privacy);
return "";
}
public String getC2Name(MembreModel model, ResultPrivacy privacy) {
if (c2_id != null)
return c2_id.getName(model, privacy);
if (c2_guest != null)
return c2_guest.getName(model, privacy);
return "";
}
public String getC1Name() {
if (c1_id != null)
return c1_id.getName();
if (c1_guest != null)
return c1_guest.getName();
return "";
}
public String getC2Name() {
if (c2_id != null)
return c2_id.getName();
if (c2_guest != null)
return c2_guest.getName();
return "";
}
public int win() {
int sum = 0;
for (ScoreEmbeddable score : this.getScores()) {
if (score.getS1() == -1000 || score.getS2() == -1000)
continue;
if (score.getS1() > score.getS2())
sum++;
else if (score.getS1() < score.getS2())
sum--;
}
return sum;
}
public CombModel getC1() {
if (this.c1_id != null)
return this.c1_id;
if (this.c1_guest != null)
return this.c1_guest;
return null;
}
public CombModel getC2() {
if (this.c2_id != null)
return this.c2_id;
if (this.c2_guest != null)
return this.c2_guest;
return null;
}
public boolean isC1(Object comb) {
if (this.c1_guest != null && this.c1_guest.isInTeam(comb))
return true;
if (comb instanceof Long id_) {
if (id_ >= 0)
return Objects.equals(this.c1_id != null ? this.c1_id.getId() : null, id_);
else
return Objects.equals(this.c1_guest != null ? this.c1_guest.getId() : null, -id_);
}
return Objects.equals(this.c1_id, comb) || Objects.equals(this.c1_guest, comb);
}
public boolean isC2(Object comb) {
if (this.c2_guest != null && this.c2_guest.isInTeam(comb))
return true;
if (comb instanceof Long id_) {
if (id_ >= 0)
return Objects.equals(this.c2_id != null ? this.c2_id.getId() : null, id_);
else
return Objects.equals(this.c2_guest != null ? this.c2_guest.getId() : null, -id_);
}
return Objects.equals(this.c2_id, comb) || Objects.equals(this.c2_guest, comb);
}
}

View File

@@ -1,14 +1,14 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.RoleAsso;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@Getter
@Setter
@@ -19,36 +19,109 @@ import java.util.Date;
@Entity
@Table(name = "membre")
public class MembreModel {
public class MembreModel implements LoggableModel, CombModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Access(AccessType.PROPERTY)
@Schema(description = "L'identifiant du membre.", examples = "1")
Long id;
@Schema(description = "L'identifiant long du membre (userID).", examples = "e81d1d35-d897-421e-8086-6c5e74d13c6e")
String userId;
@Schema(description = "Le nom du membre.", examples = "Dupont")
String lname;
@Schema(description = "Le prénom du membre.", examples = "Jean")
String fname;
@Schema(description = "La catégorie du membre.", examples = "SENIOR")
Categorie categorie;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "club", referencedColumnName = "id")
@Schema(description = "Le club du membre.")
ClubModel club;
@Schema(description = "Le genre du membre.", examples = "H")
Genre genre;
int licence;
@Schema(description = "Le numéro de licence du membre.", examples = "12345")
Integer licence;
@Schema(description = "Le pays du membre.", examples = "FR")
String country;
@Schema(description = "La date de naissance du membre.")
Date birth_date;
@Schema(description = "L'adresse e-mail du membre.", examples = "jean.dupont@examples.com")
String email;
@Schema(description = "Le rôle du membre dans l'association.", examples = "MEMBRE")
RoleAsso role;
@Schema(description = "Le grade d'arbitrage du membre.", examples = "NA")
GradeArbitrage grade_arbitrage;
String url_photo;
@Schema(description = "La confidentialité de ces résultats", examples = "PUBLIC")
@Column(nullable = false, columnDefinition = "smallint default 0")
ResultPrivacy resultPrivacy = ResultPrivacy.PUBLIC;
@OneToMany(mappedBy = "membre", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@Schema(description = "Les licences du membre. (optionnel)")
List<LicenceModel> licences;
@OneToMany(mappedBy = "membre", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@Schema(description = "Les séléctions du membre. (optionnel)")
List<SelectionModel> selections;
@Override
public String getObjectName() {
return fname + " " + lname;
}
@Override
public LogModel.ObjectType getObjectType() {
return LogModel.ObjectType.Membre;
}
@Override
public String toString() {
return "MembreModel{" +
"id=" + id +
", userId='" + userId + '\'' +
", lname='" + lname + '\'' +
", fname='" + fname + '\'' +
", categorie=" + categorie +
", genre=" + genre +
", licence=" + licence +
", country='" + country + '\'' +
", birth_date=" + birth_date +
", email='" + email + '\'' +
", role=" + role +
", grade_arbitrage=" + grade_arbitrage +
'}';
}
@Override
public Long getCombId() {
return this.id;
}
@Override
public String getName() {
return this.fname + " " + this.lname;
}
@Override
public String getName(MembreModel model, ResultPrivacy privacy) {
if (model == null || !Objects.equals(this.getId(), model.getId())) {
if (model == null && this.getResultPrivacy() != ResultPrivacy.PUBLIC)
return "Anonyme";
if (this.getResultPrivacy().ordinal() > privacy.ordinal())
return "Anonyme";
}
return getName();
}
}

View File

@@ -0,0 +1,99 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.data.id.RegisterId;
import fr.titionfire.ffsaf.utils.Categorie;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "register")
public class RegisterModel {
@EmbeddedId
RegisterId id;
@MapsId("competitionId")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_competition")
CompetitionModel competition;
@MapsId("membreId")
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "id_membre")
MembreModel membre;
Float weight;
Float weightReal;
int overCategory = 0;
Categorie categorie;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "club")
@OnDelete(action = OnDeleteAction.SET_NULL)
ClubModel club = null;
@Column(nullable = false, columnDefinition = "boolean default false")
boolean lockEdit = false;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinTable(
name = "categories_insc_comb",
joinColumns = {
@JoinColumn(name = "id_competition", referencedColumnName = "id_competition"),
@JoinColumn(name = "id_membre", referencedColumnName = "id_membre")
},
inverseJoinColumns = @JoinColumn(name = "category_id")
)
List<CatPresetModel> categoriesInscrites = new ArrayList<>();
public RegisterModel(CompetitionModel competition, MembreModel membre, Float weight, int overCategory,
Categorie categorie, ClubModel club) {
this.id = new RegisterId(competition.getId(), membre.getId());
this.competition = competition;
this.membre = membre;
this.weight = weight;
this.overCategory = overCategory;
this.categorie = categorie;
this.club = club;
}
public String getName() {
return membre.fname + " " + membre.lname;
}
public ClubModel getClub2() {
if (club == null)
return membre.club;
return club;
}
public Categorie getCategorie2() {
Categorie tmp = this.categorie;
if (tmp == null)
tmp = membre.getCategorie();
if (tmp == null)
return null;
return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)];
}
public Float getWeight2() {
if (weightReal != null)
return weightReal;
return weight;
}
}

View File

@@ -0,0 +1,44 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "selection")
public class SelectionModel implements LoggableModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "L'identifiant de la séléction.")
Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membre", referencedColumnName = "id")
@Schema(description = "Le membre de la séléction. (optionnel)")
MembreModel membre;
@Schema(description = "La saison de la séléction.", examples = "2025")
int saison;
@Schema(description = "Catégorie de la séléction.")
Categorie categorie;
@Override
public String getObjectName() {
return "selection " + id.toString();
}
@Override
public LogModel.ObjectType getObjectType() {
return LogModel.ObjectType.Selection;
}
}

View File

@@ -0,0 +1,24 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.SequenceType;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "sequence")
public class SequenceModel {
@Id
SequenceType type;
long value;
}

View File

@@ -0,0 +1,106 @@
package fr.titionfire.ffsaf.data.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "tree")
public class TreeModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name = "id_category")
Long category;
Integer level;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "match_id", referencedColumnName = "id")
MatchModel match;
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinColumn(referencedColumnName = "id")
TreeModel left;
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinColumn(referencedColumnName = "id")
TreeModel right;
public TreeModel(Long category, Integer level, MatchModel match) {
this.category = category;
this.level = level;
this.match = match;
this.left = null;
this.right = null;
}
public List<TreeModel> flat() {
List<TreeModel> out = new ArrayList<>();
this.flat(out);
return out;
}
private void flat(List<TreeModel> out) {
out.add(this);
if (this.right != null)
this.right.flat(out);
if (this.left != null)
this.left.flat(out);
}
public int death() {
int dg = 0;
int dd = 0;
if (this.right != null)
dg = this.right.death();
if (this.left != null)
dg = this.left.death();
return 1 + Math.max(dg, dd);
}
public int getMaxChildrenAtDepth(int death, int current) {
if (current == death)
return 1;
int tmp = 0;
if (this.right != null)
tmp += this.right.getMaxChildrenAtDepth(death, current + 1);
if (this.left != null)
tmp += this.left.getMaxChildrenAtDepth(death, current + 1);
return tmp;
}
public void getChildrenAtDepth (int death, int current, List<TreeModel> out) {
if (current == death) {
out.add(this);
return;
}
if (this.right != null)
this.right.getChildrenAtDepth(death, current + 1, out);
if (this.left != null)
this.left.getChildrenAtDepth(death, current + 1, out);
}
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.AffiliationModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AffiliationRepository implements PanacheRepositoryBase<AffiliationModel, Long> {
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AffiliationRequestRepository implements PanacheRepositoryBase<AffiliationRequestModel, Long> {
}

View File

@@ -0,0 +1,10 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CardModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CardRepository implements PanacheRepositoryBase<CardModel, Long> {
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CatPresetModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CatPresetRepository implements PanacheRepositoryBase<CatPresetModel, Long> {
}

View File

@@ -0,0 +1,19 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CategoryModel;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CategoryRepository implements PanacheRepositoryBase<CategoryModel, Long> {
public Uni<CategoryModel> create(CategoryModel categoryModel) {
categoryModel.setSystem(CompetitionSystem.INTERNAL);
return Panache.withTransaction(() -> this.persist(categoryModel)
.invoke(categoryModel1 -> categoryModel1.setSystemId(categoryModel1.getId())))
.chain(this::persist);
}
}

View File

@@ -0,0 +1,18 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CheckoutModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CheckoutRepository implements PanacheRepositoryBase<CheckoutModel, Long> {
public Uni<Long> countByLicenseId(long id) {
return getSession()
.chain(s -> s
.createNativeQuery("SELECT COUNT(*) FROM ffsaf__checkout WHERE ?1 = ANY(license_ids)", Long.class)
.setParameter(1, id)
.getSingleResult());
}
}

View File

@@ -0,0 +1,10 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.ClubCardModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ClubCardRepository implements PanacheRepositoryBase<ClubCardModel, Long> {
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CompetitionGuestRepository implements PanacheRepositoryBase<CompetitionGuestModel, Long> {
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CompetitionModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CompetitionRepository implements PanacheRepositoryBase<CompetitionModel, Long> {
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.HelloAssoRegisterModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class HelloAssoRegisterRepository implements PanacheRepositoryBase<HelloAssoRegisterModel, Long> {
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.LicenceModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class LicenceRepository implements PanacheRepositoryBase<LicenceModel, Long> {
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.LogModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class LogRepository implements PanacheRepositoryBase<LogModel, Long> {
}

View File

@@ -0,0 +1,33 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
@ApplicationScoped
public class MatchRepository implements PanacheRepositoryBase<MatchModel, Long> {
public Uni<MatchModel> create(MatchModel matchModel) {
matchModel.setSystem(CompetitionSystem.INTERNAL);
return Panache.withTransaction(() -> this.persistAndFlush(matchModel)
.invoke(matchModel1 -> matchModel1.setSystemId(matchModel1.getId())))
.chain(this::persist);
}
public Uni<Void> create(List<MatchModel> matchModel) {
if (matchModel.isEmpty())
return Uni.createFrom().voidItem();
matchModel.forEach(model -> model.setSystem(CompetitionSystem.INTERNAL));
return Panache.withTransaction(() -> this.persist(matchModel)
.call(__ -> this.flush())
.invoke(__ -> matchModel.forEach(model -> model.setSystemId(model.getId())))
.map(__ -> matchModel))
.chain(this::persist);
}
}

View File

@@ -0,0 +1,11 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.id.RegisterId;
import fr.titionfire.ffsaf.data.model.RegisterModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class RegisterRepository implements PanacheRepositoryBase<RegisterModel, RegisterId> {
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.SelectionModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class SelectionRepository implements PanacheRepositoryBase<SelectionModel, Long> {
}

View File

@@ -0,0 +1,21 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.SequenceModel;
import fr.titionfire.ffsaf.utils.SequenceType;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class SequenceRepository implements PanacheRepositoryBase<SequenceModel, SequenceType> {
public Uni<Long> getNextValueInTransaction(SequenceType type) {
return this.findById(type).onItem().ifNull()
.switchTo(() -> this.persist(new SequenceModel(type, 1L)))
.chain(v -> {
v.setValue(v.getValue() + 1);
return this.persistAndFlush(v);
})
.map(SequenceModel::getValue);
}
}

View File

@@ -0,0 +1,23 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.TreeModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class TreeRepository implements PanacheRepositoryBase<TreeModel, Long> {
@WithTransaction
public Uni<Boolean> deleteTree(TreeModel entity) {
Uni<Boolean> uni = Uni.createFrom().item(false);
if (entity == null)
return uni;
if (entity.getLeft() != null)
uni = uni.chain(__ -> this.deleteTree(entity.getLeft()));
if (entity.getRight() != null)
uni = uni.chain(__ -> this.deleteTree(entity.getRight()));
return uni.chain(__ -> this.deleteById(entity.getId()));
}
}

View File

@@ -18,14 +18,12 @@ public class ClubEntity {
private String name;
private String clubId;
private String country;
private String shieldURL;
private Map<Contact, String> contact;
private String training_location;
private String training_day_time;
private String contact_intern;
private String RNA;
private String SIRET;
private String no_affiliation;
private String StateId;
private Long no_affiliation;
private boolean international;
public static ClubEntity fromModel (ClubModel model) {
@@ -38,21 +36,13 @@ public class ClubEntity {
.name(model.getName())
.clubId(model.getClubId())
.country(model.getCountry())
.shieldURL(model.getShieldURL())
.contact(model.getContact())
.training_location(model.getTraining_location())
.training_day_time(model.getTraining_day_time())
.contact_intern(model.getContact_intern())
.RNA(model.getRNA())
.SIRET(model.getSIRET())
.StateId(model.getStateId())
.no_affiliation(model.getNo_affiliation())
.international(model.isInternational())
.build();
}
public ClubModel toModel () {
return new ClubModel(this.id, this.clubId, this.name, this.country, this.shieldURL, this.contact, this.training_location,
this.training_day_time, this.contact_intern, this.RNA, this.SIRET, this.no_affiliation,
this.international);
}
}

View File

@@ -0,0 +1,76 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.CatPresetModel;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.model.RegisterModel;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@Data
@AllArgsConstructor
@RegisterForReflection
public class CombEntity {
private long id;
private String lname;
private String fname;
Categorie categorie;
String club_uuid;
String club_str;
Genre genre;
String country;
int overCategory;
Float weight;
List<CombEntity> teamMembers;
List<Long> categoriesInscrites;
public static CombEntity fromModel(MembreModel model) {
if (model == null)
return null;
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
model.getClub() == null ? null : model.getClub().getClubId(),
model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(),
0, null, new ArrayList<>(), new ArrayList<>());
}
public static CombEntity fromModel(CompetitionGuestModel model) {
if (model == null)
return null;
return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null,
model.getClub(), model.getGenre(), model.getCountry(), 0,
model.getWeight2() != null ? model.getWeight2() : model.getWeight(),
Stream.concat(model.getComb().stream().map(CombEntity::fromModel),
model.getGuest().stream().map(CombEntity::fromModel)).toList(),
new ArrayList<>());
}
public CombEntity addCategoriesInscrites(List<CatPresetModel> categoriesInscrites) {
if (categoriesInscrites == null)
return this;
this.categoriesInscrites = categoriesInscrites.stream().map(CatPresetModel::getId).toList();
return this;
}
public static CombEntity fromModel(RegisterModel registerModel) {
if (registerModel == null || registerModel.getMembre() == null)
return null;
MembreModel model = registerModel.getMembre();
return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(),
registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(),
registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(),
model.getCountry(), registerModel.getOverCategory(),
registerModel.getWeight2() != null ? registerModel.getWeight2() : registerModel.getWeight(),
new ArrayList<>(), new ArrayList<>());
}
}

View File

@@ -0,0 +1,38 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@RegisterForReflection
public class MatchEntity {
private long id;
private CombEntity c1;
private CombEntity c2;
private long categorie_ord = 0;
private boolean isEnd;
private long categorie;
private Date date;
private List<ScoreEmbeddable> scores;
private char poule;
public static MatchEntity fromModel(MatchModel model) {
if (model == null)
return null;
return new MatchEntity(model.getId(),
(model.getC1_id() == null) ? CombEntity.fromModel(model.getC1_guest()) : CombEntity.fromModel(
model.getC1_id()),
(model.getC2_id() == null) ? CombEntity.fromModel(model.getC2_guest()) : CombEntity.fromModel(
model.getC2_id()),
model.getCategory_ord(), model.isEnd(), model.getCategory().getId(), model.getDate(),
model.getScores(),
model.getPoule());
}
}

View File

@@ -0,0 +1,184 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Getter;
import java.util.*;
@RegisterForReflection
public class MatchModelExtend {
final MatchModel match;
@Getter
boolean isEnd = false;
@Getter
List<ScoreEmbeddable> scoresToPrint = new ArrayList<>();
@Getter
List<ScoreEmbeddable> scoresToCompute = new ArrayList<>();
@Getter
int win = 0;
public MatchModelExtend(MatchModel match, List<CardModel> cards) {
this.match = match;
List<Long> combIds = extractCombIds(match);
List<CardModel> cards2 = cards.stream().filter(c -> combIds.contains(c.getComb()) && c.hasEffect(match))
.sorted(Comparator.comparing(CardModel::getType).reversed()).toList();
for (ScoreEmbeddable score : match.getScores()) {
if (score.getS1() == -1000 || score.getS2() == -1000)
continue;
this.scoresToCompute.add(virtualScore(score, cards2, false));
}
calc_win_end(cards2);
for (ScoreEmbeddable score : match.getScores()) {
if (score.getS1() == -1000 || score.getS2() == -1000)
continue;
this.scoresToPrint.add(virtualScore(score, cards2, true));
}
if (this.isEnd && this.scoresToPrint.isEmpty()) {
this.scoresToPrint.add(virtualScore(new ScoreEmbeddable(0, 0, 0), cards2, true));
}
}
private ScoreEmbeddable virtualScore(ScoreEmbeddable score, List<CardModel> cards2, boolean toPrint) {
if (cards2.size() > 1) {
if (!Objects.equals(cards2.get(0).getComb(), cards2.get(1).getComb()))
return new ScoreEmbeddable(score.getN_round(), toPrint ? -997 : 0, toPrint ? -997 : 0);
}
if (!cards2.isEmpty()) {
if (isC1(cards2.get(0).getComb()))
return new ScoreEmbeddable(score.getN_round(), toPrint ? -997 : 0, 10);
else
return new ScoreEmbeddable(score.getN_round(), 10, toPrint ? -997 : 0);
}
if (score.getS1() < -900 && score.getS2() < -900)
return new ScoreEmbeddable(score.getN_round(), toPrint ? score.getS1() : 0, toPrint ? score.getS2() : 0);
else if (score.getS1() < -900)
return new ScoreEmbeddable(score.getN_round(), toPrint ? score.getS1() : 0, 10);
else if (score.getS2() < -900)
return new ScoreEmbeddable(score.getN_round(), 10, toPrint ? score.getS2() : 0);
return new ScoreEmbeddable(score.getN_round(), score.getS1(), score.getS2());
}
private void calc_win_end(List<CardModel> cards2) {
if (cards2.size() > 1) {
if (!Objects.equals(cards2.get(0).getComb(), cards2.get(1).getComb())) {
this.win = 0;
this.isEnd = true;
return;
}
}
if (!cards2.isEmpty()) {
if (match.isC1(cards2.get(0).getComb())) {
this.win = -1;
} else if (match.isC2(cards2.get(0).getComb())) {
this.win = 1;
}
this.isEnd = true;
return;
}
for (ScoreEmbeddable score : this.scoresToCompute) {
if (score.getS1() > score.getS2())
win++;
else if (score.getS1() < score.getS2())
win--;
}
this.isEnd = match.isEnd();
}
private List<Long> extractCombIds(MatchModel match) {
List<Long> ids = new ArrayList<>();
if (match.getC1_id() != null)
ids.add(match.getC1_id().getId());
if (match.getC2_id() != null)
ids.add(match.getC2_id().getId());
if (match.getC1_guest() != null)
ids.add(match.getC1_guest().getId() * -1);
if (match.getC2_guest() != null)
ids.add(match.getC2_guest().getId() * -1);
return ids;
}
//--------------- Delegation methods to MatchModel ---------------
public Long getId() {
return match.getId();
}
public MembreModel getC1_id() {
return match.getC1_id();
}
public CompetitionGuestModel getC1_guest() {
return match.getC1_guest();
}
public MembreModel getC2_id() {
return match.getC2_id();
}
public CompetitionGuestModel getC2_guest() {
return match.getC2_guest();
}
public CategoryModel getCategory() {
return match.getCategory();
}
public long getCategory_ord() {
return match.getCategory_ord();
}
public Date getDate() {
return match.getDate();
}
public char getPoule() {
return match.getPoule();
}
public String getC1Name(MembreModel model, ResultPrivacy privacy) {
return match.getC1Name(model, privacy);
}
public String getC2Name(MembreModel model, ResultPrivacy privacy) {
return match.getC2Name(model, privacy);
}
public String getC2Name() {
return match.getC2Name();
}
public String getC1Name() {
return match.getC1Name();
}
public boolean isC1(Object comb) {
return match.isC1(comb);
}
public boolean isC2(Object comb) {
return match.isC2(comb);
}
public CombModel getC1() {
return match.getC1();
}
public CombModel getC2() {
return match.getC2();
}
}

View File

@@ -1,64 +0,0 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.RoleAsso;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@RegisterForReflection
public class MembreEntity {
private long id;
private String lname = "";
private String fname = "";
private Categorie categorie;
private ClubEntity club;
private Genre genre;
private int licence;
private String country;
private Date birth_date;
private String email;
private RoleAsso role;
private GradeArbitrage grade_arbitrage;
private String url_photo;
public static MembreEntity fromModel(MembreModel model) {
if (model == null)
return null;
return new MembreEntityBuilder()
.id(model.getId())
.lname(model.getLname())
.fname(model.getFname())
.categorie(model.getCategorie())
.club(ClubEntity.fromModel(model.getClub()))
.genre(model.getGenre())
.licence(model.getLicence())
.country(model.getCountry())
.birth_date(model.getBirth_date())
.email(model.getEmail())
.role(model.getRole())
.grade_arbitrage(model.getGrade_arbitrage())
.url_photo(model.getUrl_photo())
.build();
}
public static String getFullName(MembreModel model) {
return model.getFname() + " " + model.getLname();
}
public String getFullName() {
return this.fname + " " + this.lname;
}
}

View File

@@ -0,0 +1,71 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.TreeModel;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@RegisterForReflection
public class TreeEntity {
private Long id;
private Long categorie;
private Integer level;
private MatchEntity match;
private TreeEntity left;
private TreeEntity right;
private TreeEntity associatedNode;
public static TreeEntity fromModel(TreeModel model) {
if (model == null)
return null;
return new TreeEntity(model.getId(), model.getCategory(), model.getLevel(), MatchEntity.fromModel(model.getMatch()), fromModel(model.getLeft()),
fromModel(model.getRight()), null);
}
public TreeEntity getMatchNode(Long matchId) {
if (this.match != null && this.match.getId() == matchId) {
return this;
} else {
if (this.left != null) {
TreeEntity left = this.left.getMatchNode(matchId);
if (left != null) {
return left;
}
}
if (this.right != null) {
TreeEntity right = this.right.getMatchNode(matchId);
if (right != null) {
return right;
}
}
}
return null;
}
public static TreeEntity getParent(TreeEntity current, TreeEntity target) {
if (current == null) {
return null;
} else if (current.equals(target)) {
return null;
} else if (target.equals(current.left) || target.equals(current.right)) {
return current;
} else {
TreeEntity left = getParent(current.left, target);
if (left != null)
return left;
return getParent(current.right, target);
}
}
public static void setAssociated(TreeEntity current, TreeEntity next) {
if (current == null || next == null) {
return;
}
current.setAssociatedNode(next);
setAssociated(current.getLeft(), next.getLeft());
setAssociated(current.getRight(), next.getRight());
}
}

View File

@@ -0,0 +1,492 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.rest.client.SirenService;
import fr.titionfire.ffsaf.rest.client.StateIdService;
import fr.titionfire.ffsaf.rest.data.SimpleAffiliation;
import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliation;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestSaveForm;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.SequenceType;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.reactive.ReactiveMailer;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
@WithSession
@ApplicationScoped
public class AffiliationService {
private static final Logger LOGGER = Logger.getLogger(AffiliationService.class);
@Inject
CombRepository combRepository;
@Inject
ClubRepository clubRepository;
@Inject
AffiliationRequestRepository repositoryRequest;
@Inject
AffiliationRepository repository;
@Inject
KeycloakService keycloakService;
@Inject
SequenceRepository sequenceRepository;
@Inject
LicenceRepository licenceRepository;
@Inject
ReactiveMailer reactiveMailer;
@Inject
LoggerService ls;
@Inject
VirusScannerService scanner;
@RestClient
StateIdService stateIdService;
@RestClient
SirenService sirenService;
@ConfigProperty(name = "upload_dir")
String media;
@ConfigProperty(name = "notif.affRequest.mail")
List<String> mails;
@Inject
TradService trad;
public Uni<List<AffiliationRequestModel>> getAllReq() {
return repositoryRequest.listAll();
}
public Uni<AffiliationRequestModel> pre_save(AffiliationRequestForm form, boolean unique) {
AffiliationRequestModel affModel = form.toModel();
int currentSaison = Utils.getSaison();
List<String> out = new ArrayList<>();
out.add(affModel.getState_id());
return Uni.createFrom().item(affModel)
.invoke(Unchecked.consumer(model -> {
if (model.getSaison() != currentSaison && model.getSaison() != currentSaison + 1) {
throw new DBadRequestException(trad.t("saison.non.valid"));
}
}))
.chain(() -> ((affModel.getState_id().charAt(0) == 'W') ? stateIdService.get_rna(
affModel.getState_id()) : sirenService.get_unite(affModel.getState_id())
.chain(stateIdService::getAssoDataFromUnit)).onItem().transform(o -> {
if (o.getRna() != null && !o.getRna().isBlank())
out.add(o.getRna());
if (o.getSiren() != null && !o.getSiren().isBlank())
out.add(o.getSiren());
if (o.getIdentite().getSiret_siege() != null && !o.getIdentite().getSiret_siege().isBlank())
out.add(o.getIdentite().getSiret_siege());
return out;
}).onFailure().recoverWithItem(out)
.chain(a -> repositoryRequest.count("state_id IN ?1 and saison = ?2",
out, affModel.getSaison()))
.onItem().invoke(Unchecked.consumer(count -> {
if (count != 0 && unique) {
throw new DBadRequestException(trad.t("demande.d.affiliation.deja.existante"));
}
}))
)
.chain(() -> clubRepository.find("StateId IN ?1", out).firstResult().chain(club ->
repository.count("club = ?1 and saison = ?2", club, affModel.getSaison())))
.onItem().invoke(Unchecked.consumer(count -> {
if (count != 0) {
throw new DBadRequestException(trad.t("affiliation.deja.existante"));
}
}))
.map(o -> affModel)
.call(model -> ((model.getM1_lincence() != -1) ? combRepository.find("licence",
model.getM1_lincence()).count().invoke(Unchecked.consumer(count -> {
if (count == 0) {
throw new DBadRequestException(trad.t("licence.membre.n.1.inconnue"));
}
})) : Uni.createFrom().nullItem())
)
.call(model -> ((model.getM2_lincence() != -1) ? combRepository.find("licence",
model.getM2_lincence()).count().invoke(Unchecked.consumer(count -> {
if (count == 0) {
throw new DBadRequestException(trad.t("licence.membre.n.2.inconnue"));
}
})) : Uni.createFrom().nullItem())
)
.call(model -> ((model.getM3_lincence() != -1) ? combRepository.find("licence",
model.getM3_lincence()).count().invoke(Unchecked.consumer(count -> {
if (count == 0) {
throw new DBadRequestException(trad.t("licence.membre.n.3.inconnue"));
}
})) : Uni.createFrom().nullItem())
);
}
public Uni<?> saveEdit(AffiliationRequestForm form) {
return pre_save(form, false)
.chain(model -> repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
.chain(origine -> {
origine.setName(model.getName());
origine.setAddress(model.getAddress());
origine.setContact(model.getContact());
origine.setM1_lname(model.getM1_lname());
origine.setM1_fname(model.getM1_fname());
origine.setM1_lincence(model.getM1_lincence());
origine.setM1_role(model.getM1_role());
origine.setM1_email(model.getM1_email());
origine.setM2_lname(model.getM2_lname());
origine.setM2_fname(model.getM2_fname());
origine.setM2_lincence(model.getM2_lincence());
origine.setM2_role(model.getM2_role());
origine.setM2_email(model.getM2_email());
origine.setM3_lname(model.getM3_lname());
origine.setM3_fname(model.getM3_fname());
origine.setM3_lincence(model.getM3_lincence());
origine.setM3_role(model.getM3_role());
origine.setM3_email(model.getM3_email());
return Panache.withTransaction(() -> repositoryRequest.persist(origine));
}));
}
public Uni<String> save(AffiliationRequestForm form) {
LOGGER.debug("Affiliation Request Created");
LOGGER.debug(form.toString());
// noinspection ReactiveStreamsUnusedPublisher
return pre_save(form, true)
.chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
.onItem()
.invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo"))
.invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status"))
.call(model -> reactiveMailer.send(
Mail.withText("no-reply@ffsaf.fr",
"[NOTIF] FFSAF - Nouvelle demande d'affiliation",
String.format(
"""
Une nouvelle demande d'affiliation a été déposée sur l'intranet pour le club: %s.
""", model.getName())
).setFrom("FFSAF <no-reply@ffsaf.fr>")
.addBcc(mails.toArray(String[]::new))
))
.map(__ -> "Ok");
}
public Uni<?> saveAdmin(AffiliationRequestSaveForm form) {
LOGGER.debug("Affiliation Request Saved");
LOGGER.debug(form.toString());
return repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
.map(model -> {
model.setName(form.getName());
model.setState_id(form.getState_id());
model.setAddress(form.getAddress());
model.setContact(form.getContact());
if (form.getM1_mode() == 2) {
model.setM1_lname(form.getM1_lname());
model.setM1_fname(form.getM1_fname());
} else {
model.setM1_lincence(
form.getM1_lincence() == null ? 0 : Integer.parseInt(form.getM1_lincence()));
}
model.setM1_role(form.getM1_role());
if (form.getM1_email_mode() == 0)
model.setM1_email(form.getM1_email());
if (form.getM2_mode() == 2) {
model.setM2_lname(form.getM2_lname());
model.setM2_fname(form.getM2_fname());
} else {
model.setM2_lincence(
form.getM2_lincence() == null ? 0 : Integer.parseInt(form.getM2_lincence()));
}
model.setM2_role(form.getM2_role());
if (form.getM2_email_mode() == 0)
model.setM2_email(form.getM2_email());
if (form.getM3_mode() == 2) {
model.setM3_lname(form.getM3_lname());
model.setM3_fname(form.getM3_fname());
} else {
model.setM3_lincence(
form.getM3_lincence() == null ? 0 : Integer.parseInt(form.getM3_lincence()));
}
model.setM3_role(form.getM3_role());
if (form.getM3_email_mode() == 0)
model.setM3_email(form.getM3_email());
return model;
})
.chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
.onItem()
.invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo"))
.invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status"))
.map(__ -> "Ok");
}
private Uni<?> setMembre(AffiliationRequestSaveForm.Member member, ClubModel club, int saison) {
return Uni.createFrom().nullItem().chain(__ -> {
if (member.getMode() == 2) {
MembreModel membreModel = new MembreModel();
membreModel.setFname(member.getFname());
membreModel.setLname(member.getLname().toUpperCase());
membreModel.setClub(club);
membreModel.setRole(member.getRole());
membreModel.setEmail(member.getEmail());
membreModel.setGrade_arbitrage(GradeArbitrage.NA);
membreModel.setGenre(Genre.NA);
membreModel.setCountry("FR");
return Panache.withTransaction(() ->
combRepository.persist(membreModel)
.chain(m -> sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
.invoke(l -> m.setLicence(Math.toIntExact(l)))
.chain(() -> combRepository.persist(m))));
} else {
return combRepository.find("licence", Integer.parseInt(member.getLicence())).firstResult()
.onItem().ifNull().switchTo(() -> {
MembreModel membreModel = new MembreModel();
membreModel.setFname(member.getFname());
membreModel.setLname(member.getLname().toUpperCase());
return Panache.withTransaction(
() -> sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
.invoke(l -> membreModel.setLicence(Math.toIntExact(l)))
.chain(() -> combRepository.persist(membreModel)));
})
.map(m -> {
m.setClub(club);
m.setRole(member.getRole());
m.setEmail(member.getEmail());
return m;
}).call(m -> Panache.withTransaction(() -> combRepository.persist(m)));
}
})
.call(m -> ((m.getUserId() == null) ? keycloakService.initCompte(m.getId())
.onFailure().invoke(t -> LOGGER.warnf("Failed to init account: %s", t.getMessage())).onFailure()
.recoverWithNull() :
keycloakService.setClubGroupMembre(m, club).map(__ -> m.getUserId()))
.call(userId -> keycloakService.setAutoRoleMembre(userId, m.getRole(), m.getGrade_arbitrage()))
.call(userId -> keycloakService.setEmail(userId, m.getEmail())))
.call(m -> Mutiny.fetch(m.getLicences())
.call(l1 -> l1 != null && l1.stream().anyMatch(l -> l.getSaison() == saison) ?
Uni.createFrom().nullItem() :
Panache.withTransaction(() -> licenceRepository.persist(
new LicenceModel(null, m, club.getId(), saison, null, true, false)))
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, m.getObjectName(),
licenceModel))));
}
public Uni<?> accept(AffiliationRequestSaveForm form) {
LOGGER.debug("Affiliation Request Accepted");
LOGGER.debug(form.toString());
return repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
.chain(req ->
clubRepository.find("StateId = ?1", form.getState_id()).firstResult()
.chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model))
.call(club -> setMembre(form.new Member(1), club, req.getSaison()).onFailure()
.recoverWithNull()
.call(__ -> setMembre(form.new Member(2), club, req.getSaison()).onFailure()
.recoverWithNull()
.call(___ -> setMembre(form.new Member(3), club, req.getSaison()))))
.onItem()
.invoke(model -> Utils.uploadFile(scanner, form.getLogo(), form.getId(), media,
"aff_request/logo"))
.invoke(model -> Utils.uploadFile(scanner, form.getStatus(), form.getId(), media,
"aff_request/status"))
.onItem()
.call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/logo",
"ppClub"))
.call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/status",
"clubStatus"))
)
.map(__ -> "Ok");
}
private Uni<ClubModel> acceptNew(AffiliationRequestSaveForm form, AffiliationRequestModel model) {
LOGGER.debug("New Club Accepted");
return Uni.createFrom().nullItem()
.chain(() -> {
ClubModel club = new ClubModel();
club.setName(form.getName());
club.setCountry("FR");
club.setStateId(form.getState_id());
club.setAddress(form.getAddress());
club.setContact_intern(form.getContact());
club.setAffiliations(new ArrayList<>());
return Panache.withTransaction(() -> clubRepository.persist(club)
.chain(c -> sequenceRepository.getNextValueInTransaction(SequenceType.Affiliation)
.invoke(c::setNo_affiliation)
.chain(() -> clubRepository.persist(c))
.chain(() -> repositoryRequest.delete(model))
)
.chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison())))
.map(c -> club));
})
.call(club -> reactiveMailer.send(
Mail.withHtml(form.getM1_email(), "FFSAF - Creation de votre compte sur l'intranet",
String.format(Utils.HTML_HEADER, "FFSAF - Creation de votre compte sur l'intranet") +
String.format("""
<p>Votre demande d'affiliation pour le club <span class="highlight">%s</span> a été acceptée.</p>
<p>LLe numéro d'affiliation de votre club est le <span class="highlight">%d</span>.</p>
""",
club.getName(), club.getNo_affiliation()) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour,
Votre demande d'affiliation pour le club %s a été acceptée.
Le numéro d'affiliation de votre club est le %d.
Cordialement,
L'équipe de la FFSAF
""", club.getName(), club.getNo_affiliation()))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(form.getM2_email(), form.getM3_email())
));
}
private Uni<ClubModel> acceptOld(AffiliationRequestSaveForm form, AffiliationRequestModel model, ClubModel club) {
AtomicBoolean nameChange = new AtomicBoolean(false);
LOGGER.debug("Old Club Accepted");
return Uni.createFrom().nullItem()
.chain(() -> {
if (!form.getName().equals(club.getName())) {
club.setName(form.getName());
nameChange.set(true);
}
club.setCountry("FR");
club.setStateId(form.getState_id());
club.setAddress(form.getAddress());
club.setContact_intern(form.getContact());
return Panache.withTransaction(() -> clubRepository.persist(club)
.chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison())))
.chain(() -> repositoryRequest.delete(model)))
.call(() -> nameChange.get() ? keycloakService.updateGroupFromClub(
club) // update group in keycloak
: Uni.createFrom().nullItem());
})
.map(__ -> club);
}
public Uni<SimpleReqAffiliation> getRequest(long id) {
return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel)
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
.call(out -> clubRepository.find("StateId = ?1", out.getStateId()).firstResult().invoke(c -> {
if (c != null) {
out.setClub(c.getId());
out.setClub_name(c.getName());
out.setClub_no_aff(c.getNo_affiliation());
}
})
);
}
public Uni<List<SimpleAffiliation>> getCurrentSaisonAffiliation() {
return repositoryRequest.list("saison = ?1 or saison = ?1 + 1", Utils.getSaison())
.map(models -> models.stream()
.map(model -> new SimpleAffiliation(model.getId() * -1, model.getState_id(), model.getSaison(),
false)).toList())
.chain(aff -> repository.list("saison = ?1", Utils.getSaison())
.map(models -> models.stream().map(SimpleAffiliation::fromModel).toList())
.map(aff2 -> Stream.concat(aff2.stream(), aff.stream()).toList())
);
}
public Uni<List<SimpleAffiliation>> getAffiliation(long id) {
return clubRepository.findById(id)
.onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve")))
.call(model -> Mutiny.fetch(model.getAffiliations()))
.chain(model -> repositoryRequest.list("state_id = ?1", model.getStateId())
.map(reqs -> reqs.stream().map(req ->
new SimpleAffiliation(req.getId() * -1, model.getStateId(), req.getSaison(), false)))
.map(aff2 -> Stream.concat(aff2,
model.getAffiliations().stream().map(SimpleAffiliation::fromModel)).toList())
);
}
public Uni<SimpleAffiliation> setAffiliation(long id, int saison) {
return clubRepository.findById(id)
.onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve")))
.call(model -> Mutiny.fetch(model.getAffiliations()))
.invoke(Unchecked.consumer(club -> {
if (club.getAffiliations().stream().anyMatch(affiliation -> affiliation.getSaison() == saison)) {
throw new DBadRequestException(trad.t("affiliation.deja.existante"));
}
}))
.chain(club ->
Panache.withTransaction(() -> repository.persist(new AffiliationModel(null, club, saison))
.chain(c -> (club.getNo_affiliation() != null) ? Uni.createFrom().item(c) :
sequenceRepository.getNextValueInTransaction(SequenceType.Affiliation)
.invoke(club::setNo_affiliation)
.chain(() -> clubRepository.persist(club))
.map(o -> c)
)))
.map(SimpleAffiliation::fromModel);
}
public Uni<?> deleteAffiliation(long id) {
return Panache.withTransaction(() -> repository.deleteById(id));
}
public Uni<?> deleteReqAffiliation(long id, String reason, boolean federationAdmin) {
return repositoryRequest.findById(id)
.call(aff -> federationAdmin ? reactiveMailer.send(
Mail.withHtml(aff.getM1_email(), "FFSAF - Votre demande d'affiliation a été rejetée.",
String.format(Utils.HTML_HEADER, "FFSAF - Votre demande d'affiliation a été rejetée.") +
String.format("""
<p>Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):<br/>%s</p>
<p>Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@ffsaf.fr">contact@ffsaf.fr</a>.</p>
""",
aff.getName(), reason) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour,
Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):
%s
Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse contact@ffsaf.fr.
Cordialement,
L'équipe de la FFSAF
""", aff.getName(), reason))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(aff.getM2_email(), aff.getM3_email())
) : Uni.createFrom().nullItem())
.chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff)))
.call(__ -> Utils.deleteMedia(id, media, "aff_request/logo"))
.call(__ -> Utils.deleteMedia(id, media, "aff_request/status"));
}
}

View File

@@ -0,0 +1,227 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.CardModel;
import fr.titionfire.ffsaf.data.model.ClubCardModel;
import fr.titionfire.ffsaf.data.model.CompetitionModel;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.ws.recv.RCard;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.panache.common.Sort;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@WithSession
@ApplicationScoped
public class CardService {
@Inject
CardRepository cardRepository;
@Inject
ClubCardRepository clubCardRepository;
@Inject
RegisterRepository registerRepository;
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
MatchRepository matchRepository;
@Inject
TradService trad;
private static final List<CardModel.CardType> COMPETITION_LEVEL_CARDS = List.of(
CardModel.CardType.YELLOW,
CardModel.CardType.RED,
CardModel.CardType.BLACK
);
private List<Long> extractCombIds(MatchModel match) {
List<Long> ids = new ArrayList<>();
if (match.getC1_id() != null)
ids.add(match.getC1_id().getId());
if (match.getC2_id() != null)
ids.add(match.getC2_id().getId());
if (match.getC1_guest() != null)
ids.add(match.getC1_guest().getId() * -1);
if (match.getC2_guest() != null)
ids.add(match.getC2_guest().getId() * -1);
return ids;
}
public Uni<List<CardModel>> getForMatch(MatchModel match) {
return cardRepository.list(
"competition = ?1 AND (type IN ?2 OR (type = CardType.BLUE AND category = ?4)) AND comb IN ?3",
match.getCategory().getCompet(), COMPETITION_LEVEL_CARDS,
extractCombIds(match), match.getCategory().getId());
}
public Uni<List<CardModel>> getAll(CompetitionModel competition) {
return cardRepository.list("competition = ?1", competition);
}
public Uni<RCard.SendCardAdd> checkCanBeAdded(RCard.SendCardAdd card, MatchModel matchModel) {
return cardRepository.find("competition = ?1 AND comb = ?2",
Sort.descending("type"),
matchModel.getCategory().getCompet(), card.combId())
.firstResult()
.map(card_ -> {
if (card.type() == CardModel.CardType.BLUE) {
return card_ == null || (card_.getType() == CardModel.CardType.BLUE
&& !Objects.equals(card_.getCategory(), matchModel.getCategory().getId()));
}
if (card.type() == CardModel.CardType.BLACK) {
return card_ != null && card_.getType() == CardModel.CardType.RED;
}
return card_ == null || card_.getType().ordinal() < card.type().ordinal();
})
.chain(b -> {
if (b)
return Uni.createFrom().item(card);
else
return Uni.createFrom().failure(new DBadRequestException(trad.t("card.cannot.be.added")));
});
}
public Uni<List<CardModel>> addTeamCartToNewComb(Long combId, String teamUuid, String teamName,
CompetitionModel competition) {
return clubCardRepository.list("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3)",
Sort.ascending("type"), competition.getId(), teamUuid, teamName)
.chain(clubCards -> {
Uni<?> queue = Uni.createFrom().voidItem();
List<CardModel> addCards = new ArrayList<>();
for (ClubCardModel clubCard : clubCards) {
CardModel model = new CardModel();
model.setCompetition(competition);
model.setCompetitionId(competition.getId());
model.setComb(combId);
model.setTeamCard(true);
model.setType(clubCard.getType());
model.setDate(clubCard.getDate());
queue = queue.call(__ -> Panache.withTransaction(() -> cardRepository.persist(model))
.invoke(addCards::add)
.call(() -> {
clubCard.getCardIds().add(model.getId());
return Panache.withTransaction(() -> clubCardRepository.persist(clubCard));
}));
}
return queue.replaceWith(addCards);
});
}
public Uni<Void> rmTeamCardFromComb(Long combId, String uuid) {
return cardRepository.delete("comb = ?1 AND competition.uuid = ?2 AND teamCard = True", combId, uuid)
.replaceWithVoid();
}
public Uni<List<CardModel>> addTeamCard(CompetitionModel competition, String teamUuid, String teamName,
CardModel.CardType type, String reason) {
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3)",
Sort.descending("type"), competition.getId(), teamUuid, teamName)
.firstResult()
.map(card_ -> {
if (type == CardModel.CardType.BLACK) {
return card_ != null && card_.getType() == CardModel.CardType.RED;
}
return card_ == null || card_.getType().ordinal() < type.ordinal();
})
.chain(b -> {
if (!b)
return Uni.createFrom().failure(new DBadRequestException(trad.t("card.cannot.be.added")));
if (teamUuid != null) {
return registerRepository.list("competition = ?1 AND club.clubId = ?2", competition, teamUuid)
.map(l -> l.stream().map(r -> r.getMembre().getId()).toList());
} else {
return competitionGuestRepository.list("competition = ?1 AND club = ?2", competition,
teamName)
.map(l -> l.stream().map(r -> r.getId() * -1).toList());
}
})
.chain(combIds -> cardRepository.list("competition = ?1 AND comb IN ?2", competition, combIds)
.map(cards -> {
List<CardModel> newCards = new ArrayList<>();
for (Long id : combIds) {
Optional<CardModel> optional = cards.stream()
.filter(c -> id.equals(c.getComb()) && c.getType() == type).findAny();
CardModel model = new CardModel();
model.setCompetition(competition);
model.setCompetitionId(competition.getId());
model.setComb(id);
model.setTeamCard(true);
if (optional.isEmpty()) {
model.setType(type);
} else {
model.setType(
CardModel.CardType.values()[Math.min(optional.get().getType().ordinal() + 1,
CardModel.CardType.BLACK.ordinal())]);
}
newCards.add(model);
}
return newCards;
})
)
.call(newCards -> Panache.withTransaction(() -> cardRepository.persist(newCards)))
.call(newCards -> {
ClubCardModel model = new ClubCardModel();
model.setCompetition(competition.getId());
model.setTeamUuid(teamUuid);
model.setTeamName(teamName);
model.setType(type);
model.setReason(reason);
model.setCardIds(newCards.stream().map(CardModel::getId).toList());
return Panache.withTransaction(() -> clubCardRepository.persist(model));
});
}
public Uni<List<CardModel>> recvReturnState(CompetitionModel competition, RCard.SendTeamCardReturnState state) {
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3) AND type = ?4",
competition.getId(), state.teamUuid(), state.teamName(), state.type())
.firstResult()
.chain(o -> cardRepository.list("id IN ?1", o.getCardIds()))
.call(cards -> matchRepository.list("category.compet = ?1 AND category.id IN ?2", competition,
state.selectedCategory())
.invoke(matches -> {
for (CardModel card : cards) {
for (MatchModel m : matches.stream()
.filter(m -> extractCombIds(m).contains(card.getComb())).toList()) {
if (state.state() == 1) {
card.setCategory(m.getCategory().getId());
} else if (state.state() == 2) {
card.setCategory(m.getCategory().getId());
if (Objects.equals(m.getId(), state.selectedMatch()))
card.setMatch(m.getId());
}
}
}
})
.chain(() -> Panache.withTransaction(() -> cardRepository.persist(cards))));
}
public Uni<List<Long>> rmTeamCard(CompetitionModel competition, String teamUuid, String teamName,
CardModel.CardType type) {
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3) AND type = ?4",
competition.getId(), teamUuid, teamName, type)
.firstResult()
.chain(card -> Uni.createFrom().item(card.getCardIds())
.call(() -> Panache.withTransaction(() -> cardRepository.delete("id IN ?1", card.getCardIds())))
.call(() -> Panache.withTransaction(() -> clubCardRepository.delete(card))));
}
}

View File

@@ -0,0 +1,279 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.rest.data.CategoryData;
import fr.titionfire.ffsaf.rest.data.CategoryFullData;
import fr.titionfire.ffsaf.rest.data.TreeData;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
@WithSession
@ApplicationScoped
public class CategoryService {
@Inject
CategoryRepository repository;
@Inject
CompetitionRepository competRepository;
@Inject
MatchRepository matchRepository;
@Inject
TreeRepository treeRepository;
@Inject
CombRepository combRepository;
@Inject
CompetPermService permService;
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
TradService trad;
public Uni<CategoryData> getByIdAdmin(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
return repository.find("systemId = ?1 AND system = ?2", id, system)
.firstResult()
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("categorie.non.trouver")))
.call(data -> permService.hasAdminViewPerm(securityCtx, data.getCompet()))
.map(CategoryData::fromModel);
}
public Uni<List<CategoryData>> getAllAdmin(SecurityCtx securityCtx, CompetitionSystem system) {
return permService.getAllHaveAdminAccess(securityCtx)
.chain(ids -> repository.list("system = ?1 AND compet.id IN ?2", system, ids))
.map(pouleModels -> pouleModels.stream().map(CategoryData::fromModel).toList());
}
public Uni<CategoryData> addOrUpdate(SecurityCtx securityCtx, CompetitionSystem system, CategoryData data) {
return repository.find("systemId = ?1 AND system = ?2", data.getId(), system).firstResult()
.chain(o -> {
if (o == null) {
return competRepository.findById(data.getCompet())
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("competition.not.found")))
.call(o2 -> permService.hasEditPerm(securityCtx, o2))
.chain(competitionModel -> {
CategoryModel model = new CategoryModel();
model.setId(null);
model.setSystem(system);
model.setSystemId(data.getId());
model.setCompet(competitionModel);
model.setName(data.getName());
model.setMatchs(new ArrayList<>());
model.setTree(new ArrayList<>());
model.setType(data.getType());
return Panache.withTransaction(() -> repository.persist(model));
});
} else {
o.setName(data.getName());
o.setType(data.getType());
return Panache.withTransaction(() -> repository.persist(o));
}
}).map(CategoryData::fromModel);
}
private MatchModel findMatch(List<MatchModel> matchModelList, Long id) {
return matchModelList.stream().filter(m -> m.getSystemId().equals(id))
.findFirst().orElse(null);
}
private TreeModel findNode(List<TreeModel> node, Long match_id) {
return node.stream().filter(m -> m.getMatch().getSystemId().equals(match_id))
.findFirst().orElse(null);
}
private void flatTreeChild(TreeModel current, ArrayList<TreeModel> node) {
if (current != null) {
node.add(current);
flatTreeChild(current.getLeft(), node);
flatTreeChild(current.getRight(), node);
}
}
private void flatTreeChild(TreeData current, ArrayList<TreeData> node) {
if (current != null) {
node.add(current);
flatTreeChild(current.getLeft(), node);
flatTreeChild(current.getRight(), node);
}
}
private Uni<TreeModel> persisteTree(TreeData data, List<TreeModel> node, CategoryModel poule,
List<MatchModel> matchModelList) {
TreeModel mm = findNode(node, data.getMatch());
if (mm == null) {
mm = new TreeModel();
mm.setId(null);
}
mm.setLevel(data.getLevel());
mm.setCategory(poule.getId());
mm.setMatch(findMatch(matchModelList, data.getMatch()));
return Uni.createFrom().item(mm)
.call(o -> (data.getLeft() == null ? Uni.createFrom().nullItem().invoke(o1 -> o.setLeft(null)) :
persisteTree(data.getLeft(), node, poule, matchModelList).invoke(o::setLeft)))
.call(o -> (data.getRight() == null ? Uni.createFrom().nullItem().invoke(o1 -> o.setRight(null)) :
persisteTree(data.getRight(), node, poule, matchModelList).invoke(o::setRight)))
.chain(o -> Panache.withTransaction(() -> treeRepository.persist(o)));
}
public Uni<?> syncCategory(SecurityCtx securityCtx, CompetitionSystem system, CategoryFullData data) {
return repository.find("systemId = ?1 AND system = ?2", data.getId(), system)
.firstResult()
.onItem().ifNotNull().call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet()))
.onItem().ifNull().switchTo(
() -> competRepository.findById(data.getCompet())
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("competition.not.found")))
.call(o -> permService.hasEditPerm(securityCtx, o))
.map(o -> {
CategoryModel model = new CategoryModel();
model.setId(null);
model.setSystem(system);
model.setSystemId(data.getId());
model.setMatchs(new ArrayList<>());
model.setTree(new ArrayList<>());
model.setCompet(o);
return model;
}))
.call(o -> Mutiny.fetch(o.getMatchs()))
.call(o -> Mutiny.fetch(o.getTree()))
.map(o -> {
o.setName(data.getName());
o.setType(data.getType());
WorkData workData = new WorkData();
workData.category = o;
return workData;
})
.call(o -> Panache.withTransaction(() -> repository.persist(o.category)))
.call(o -> (data.getMatches() == null || data.getMatches().isEmpty()) ? Uni.createFrom().nullItem() :
Uni.createFrom()
.item(data.getMatches().stream().flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id())
.filter(Objects::nonNull)).distinct().toList())
.chain(ids -> ids.isEmpty() ? Uni.createFrom().nullItem()
: combRepository.list("id IN ?1", ids)
.invoke(o2 -> o2.forEach(m -> o.membres.put(m.getId(), m)))
)
)
.call(o -> Mutiny.fetch(o.category.getCompet().getGuests())
.invoke(o2 -> o2.forEach(m -> o.guest.put(m.getFname() + " " + m.getLname(), m)))
.map(o2 -> data.getMatches().stream().flatMap(m -> Stream.of(m.getC1_str(), m.getC2_str())
.filter(Objects::nonNull)).distinct().filter(s -> !o.guest.containsKey(s)).map(
CompetitionGuestModel::new).toList())
.call(o3 -> o3.isEmpty() ? Uni.createFrom().nullItem() :
Uni.join().all(o3.stream().map(o4 -> competitionGuestRepository.persist(o4)).toList())
.andFailFast())
.invoke(o2 -> o2.forEach(m -> o.guest.put(m.getFname() + " " + m.getLname(), m))))
.invoke(in -> {
ArrayList<TreeModel> node = new ArrayList<>();
for (TreeModel treeModel : in.category.getTree())
flatTreeChild(treeModel, node);
ArrayList<TreeData> new_node = new ArrayList<>();
for (TreeData treeModel : data.getTrees())
flatTreeChild(treeModel, new_node);
in.toRmNode = node.stream().filter(m -> new_node.stream()
.noneMatch(m2 -> m2.getMatch().equals(m.getMatch().getSystemId())))
.map(TreeModel::getId).toList();
in.unlinkNode = node;
in.unlinkNode.forEach(n -> {
n.setRight(null);
n.setLeft(null);
});
in.toRmMatch = in.category.getMatchs().stream()
.filter(m -> data.getMatches().stream().noneMatch(m2 -> m2.getId().equals(m.getSystemId())))
.map(MatchModel::getId).toList();
})
.call(in -> in.unlinkNode.isEmpty() ? Uni.createFrom().nullItem() :
Panache.withTransaction(() -> treeRepository.persist(in.unlinkNode)))
.call(in -> in.toRmNode.isEmpty() ? Uni.createFrom().nullItem() :
Panache.withTransaction(() -> treeRepository.delete("id IN ?1", in.toRmNode)))
.call(in -> in.toRmMatch.isEmpty() ? Uni.createFrom().nullItem() :
Panache.withTransaction(() -> Uni.join().all(
in.toRmMatch.stream().map(l -> matchRepository.deleteById(l)).toList())
.andCollectFailures()))
.call(in -> data.getMatches().isEmpty() ? Uni.createFrom().nullItem() :
Uni.join().all(
data.getMatches().stream().map(m -> {
MatchModel mm = findMatch(in.category.getMatchs(), m.getId());
if (mm == null) {
mm = new MatchModel();
mm.setId(null);
mm.setSystem(system);
mm.setSystemId(m.getId());
}
mm.setCategory(in.category);
mm.setCategory_ord(m.getCategory_ord());
mm.setC1_guest(in.guest.getOrDefault(m.getC1_str(), null));
mm.setC2_guest(in.guest.getOrDefault(m.getC2_str(), null));
mm.setC1_id(in.membres.getOrDefault(m.getC1_id(), null));
mm.setC2_id(in.membres.getOrDefault(m.getC2_id(), null));
mm.setEnd(m.isEnd());
mm.setPoule(m.getPoule());
mm.getScores().clear();
mm.getScores().addAll(m.getScores());
MatchModel finalMm = mm;
return Panache.withTransaction(() -> matchRepository.persist(finalMm)
.invoke(o -> in.match.add(o)));
}).toList())
.andCollectFailures())
.call(in -> data.getTrees().isEmpty() ? Uni.createFrom().nullItem() :
Uni.join().all(data.getTrees().stream()
.map(m -> persisteTree(m, in.category.getTree(), in.category, in.match)).toList())
.andCollectFailures())
.map(__ -> "OK");
}
private static class WorkData {
CategoryModel category;
HashMap<Long, MembreModel> membres = new HashMap<>();
HashMap<String, CompetitionGuestModel> guest = new HashMap<>();
List<MatchModel> match = new ArrayList<>();
List<Long> toRmMatch;
List<TreeModel> unlinkNode;
List<Long> toRmNode;
}
public Uni<?> delete(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult()
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("categorie.non.trouver")))
.call(o -> permService.hasEditPerm(securityCtx, o.getCompet()))
.call(o -> Mutiny.fetch(o.getTree())
.call(o2 -> o2.isEmpty() ? Uni.createFrom().nullItem() :
Uni.createFrom().item(o2.stream().peek(m -> {
m.setRight(null);
m.setLeft(null);
}).toList())
.call(in -> Panache.withTransaction(() -> treeRepository.persist(in)))
.map(in -> in.stream().map(TreeModel::getId).toList())
.call(in -> in.isEmpty() ? Uni.createFrom().nullItem() :
Panache.withTransaction(() -> treeRepository.delete("id IN ?1", in)))
)
)
.call(o -> matchRepository.delete("category.id = ?1", o.getId()))
.chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId())));
}
}

View File

@@ -0,0 +1,175 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.CheckoutModel;
import fr.titionfire.ffsaf.data.model.LogModel;
import fr.titionfire.ffsaf.data.repository.CheckoutRepository;
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
import fr.titionfire.ffsaf.rest.client.HelloAssoService;
import fr.titionfire.ffsaf.rest.client.dto.CheckoutIntentsRequest;
import fr.titionfire.ffsaf.rest.client.dto.CheckoutIntentsResponse;
import fr.titionfire.ffsaf.rest.client.dto.CheckoutMetadata;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.scheduler.Scheduled;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@WithSession
@ApplicationScoped
public class CheckoutService {
@Inject
CheckoutRepository repository;
@Inject
LicenceRepository licenceRepository;
@Inject
MembreService membreService;
@Inject
LicenceService licenceService;
@Inject
LoggerService ls;
@RestClient
HelloAssoService helloAssoService;
@ConfigProperty(name = "frontRootUrl")
String frontRootUrl;
@ConfigProperty(name = "unitLicencePrice")
int unitLicencePrice;
@ConfigProperty(name = "helloasso.organizationSlug")
String organizationSlug;
public Uni<Boolean> canDeleteLicence(long id) {
return repository.countByLicenseId(id).invoke(c -> System.out.println(c)).map(count -> count == 0);
}
public Uni<String> create(List<Long> ids, SecurityCtx securityCtx) {
return membreService.getByAccountId(securityCtx.getSubject())
.call(membreModel -> Mutiny.fetch(membreModel.getClub()))
.chain(membreModel -> {
CheckoutModel model = new CheckoutModel();
model.setMembre(membreModel);
model.setLicenseIds(ids);
model.setPaymentStatus(CheckoutModel.PaymentStatus.UNKNOW);
return Panache.withTransaction(() -> repository.persist(model));
})
.chain(checkoutModel -> {
CheckoutIntentsRequest request = new CheckoutIntentsRequest();
request.setTotalAmount(unitLicencePrice * checkoutModel.getLicenseIds().size());
request.setInitialAmount(unitLicencePrice * checkoutModel.getLicenseIds().size());
request.setItemName("%d licences %d-%d pour %s".formatted(checkoutModel.getLicenseIds().size(),
Utils.getSaison(), Utils.getSaison() + 1, checkoutModel.getMembre().getClub().getName()));
request.setBackUrl(frontRootUrl + "/club/member/pay");
request.setErrorUrl(frontRootUrl + "/club/member/pay/error");
request.setReturnUrl(frontRootUrl + "/club/member/pay/return");
request.setContainsDonation(false);
request.setPayer(new CheckoutIntentsRequest.Payer(checkoutModel.getMembre().getFname(),
checkoutModel.getMembre().getLname(), checkoutModel.getMembre().getEmail()));
request.setMetadata(new CheckoutMetadata(checkoutModel.getId()));
return helloAssoService.checkout(organizationSlug, request)
.call(response -> {
checkoutModel.setCheckoutId(response.getId());
return Panache.withTransaction(() -> repository.persist(checkoutModel));
});
})
.onFailure().transform(t -> new DInternalError(t.getMessage()))
.map(CheckoutIntentsResponse::getRedirectUrl);
}
public Uni<Response> paymentStatusChange(String state, CheckoutMetadata metadata) {
return repository.findById(metadata.getCheckoutDBId())
.chain(checkoutModel -> {
CheckoutModel.PaymentStatus newStatus = CheckoutModel.PaymentStatus.valueOf(state.toUpperCase());
Uni<?> uni = Uni.createFrom().nullItem();
if (checkoutModel.getPaymentStatus().equals(newStatus))
return uni;
if (newStatus.equals(CheckoutModel.PaymentStatus.AUTHORIZED)) {
for (Long id : checkoutModel.getLicenseIds()) {
uni = uni.chain(__ -> licenceRepository.findById(id)
.onFailure().recoverWithNull()
.call(licenceModel -> {
if (licenceModel == null) {
ls.logAnonymous(LogModel.ActionType.UPDATE, LogModel.ObjectType.Licence,
"Fail to save payment for licence (checkout n°" + checkoutModel.getCheckoutId() + ")",
"", id);
return Uni.createFrom().nullItem();
}
ls.logUpdateAnonymous("Paiement de la licence", licenceModel);
licenceModel.setPay(true);
if (licenceModel.getCertificate() != null && licenceModel.getCertificate()
.length() > 3) {
if (!licenceModel.isValidate())
ls.logUpdateAnonymous("Validation automatique de la licence",
licenceModel);
return licenceService.validateLicences(licenceModel);
} else {
return Panache.withTransaction(
() -> licenceRepository.persist(licenceModel));
}
}));
}
} else if (checkoutModel.getPaymentStatus().equals(CheckoutModel.PaymentStatus.AUTHORIZED)) {
for (Long id : checkoutModel.getLicenseIds()) {
uni = uni.chain(__ -> licenceRepository.findById(id)
.onFailure().recoverWithNull()
.call(licenceModel -> {
if (licenceModel == null)
return Uni.createFrom().nullItem();
ls.logUpdateAnonymous("Annulation automatique du paiement de la licence",
licenceModel);
licenceModel.setPay(false);
if (licenceModel.isValidate())
ls.logUpdateAnonymous(
"Annulation automatique de la validation de la licence",
licenceModel);
licenceModel.setValidate(false);
return Panache.withTransaction(() -> licenceRepository.persist(licenceModel));
}));
}
}
uni = uni.call(__ -> ls.append());
checkoutModel.setPaymentStatus(newStatus);
return uni.chain(__ -> Panache.withTransaction(() -> repository.persist(checkoutModel)));
})
.onFailure().invoke(Throwable::printStackTrace)
.map(__ -> Response.ok().build());
}
@Scheduled(cron = "0 0 * * * ?")
Uni<Void> everyHours() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR, -1);
Date dateLimit = calendar.getTime();
return repository.delete("creationDate < ?1 AND (checkoutId IS NULL OR paymentStatus = ?2)", dateLimit,
CheckoutModel.PaymentStatus.UNKNOW)
.map(__ -> null);
}
}

View File

@@ -1,17 +1,42 @@
package fr.titionfire.ffsaf.domain.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import fr.titionfire.ffsaf.data.model.AffiliationModel;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.AffiliationRepository;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import fr.titionfire.ffsaf.net2.request.SReqClub;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.rest.from.FullClubForm;
import fr.titionfire.ffsaf.rest.from.PartClubForm;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.vertx.VertxContextSupport;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.Collection;
import java.util.List;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
@WithSession
@ApplicationScoped
@@ -20,13 +45,36 @@ public class ClubService {
@Inject
ClubRepository repository;
@Inject
AffiliationRepository affiliationRepository;
@Inject
ServerCustom serverCustom;
@Inject
CombRepository combRepository;
@Inject
KeycloakService keycloakService;
@ConfigProperty(name = "upload_dir")
String media;
@Inject
LoggerService ls;
@Inject
TradService trad;
public SimpleClubModel findByIdOptionalClub(long id) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
return VertxContextSupport.subscribeAndAwait(
() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
}
public Collection<SimpleClubModel> findAllClub() throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(
() -> repository.findAll().list().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList())));
() -> repository.findAll().list()
.map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList())));
}
public Uni<List<ClubModel>> getAll() {
@@ -35,8 +83,281 @@ public class ClubService {
public Uni<?> setClubId(Long id, String id1) {
return repository.findById(id).chain(clubModel -> {
ls.logChange("KC UUID", clubModel.getClubId(), id1, clubModel);
clubModel.setClubId(id1);
return Panache.withTransaction(() -> repository.persist(clubModel));
return Panache.withTransaction(() -> repository.persist(clubModel))
.call(() -> ls.append());
});
}
public Uni<PageResult<SimpleClubList>> search(Integer limit, int page, String search, String country) {
if (search == null)
search = "";
search = search + "%";
PanacheQuery<ClubModel> query;
if (country == null || country.isBlank())
query = repository.find("LOWER(name) LIKE LOWER(?1)",
Sort.ascending("name"), search).page(Page.ofSize(limit));
else
query = repository.find("LOWER(name) LIKE LOWER(?1) AND country LIKE ?2",
Sort.ascending("name"), search, country + "%").page(Page.ofSize(limit));
return getPageResult(query, limit, page);
}
private Uni<PageResult<SimpleClubList>> getPageResult(PanacheQuery<ClubModel> query, int limit, int page) {
return Uni.createFrom().item(new PageResult<SimpleClubList>())
.invoke(result -> result.setPage(page))
.invoke(result -> result.setPage_size(limit))
.call(result -> query.count().invoke(result::setResult_count))
.call(result -> query.pageCount()
.invoke(Unchecked.consumer(pages -> {
if (page > pages) throw new DBadRequestException("Page out of range");
}))
.invoke(result::setPage_count))
.call(result -> query.page(Page.of(page, limit)).list()
.map(membreModels -> membreModels.stream().map(SimpleClubList::fromModel).toList())
.invoke(result::setResult));
}
public Uni<ClubModel> getById(long id) {
return repository.findById(id).call(m -> Mutiny.fetch(m.getContact()));
}
public Uni<ClubModel> getByClubId(String clubId) {
return repository.find("clubId", clubId).firstResult();
}
public Uni<ClubModel> getOfUser(SecurityCtx securityCtx) {
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
.invoke(Unchecked.consumer(m -> {
if (m == null || m.getClub() == null)
throw new DNotFoundException(trad.t("club.non.trouve"));
}))
.map(MembreModel::getClub)
.call(club -> Mutiny.fetch(club.getContact()));
}
public Uni<List<DeskMember>> getClubDesk(Consumer<ClubModel> consumer, long id) {
return repository.findById(id).invoke(consumer)
.chain(club -> combRepository.list("club = ?1", club))
.map(combs -> combs.stream()
.filter(o -> o.getRole() != null && o.getRole().level >= RoleAsso.MEMBREBUREAU.level)
.sorted((o1, o2) -> o2.getRole().level - o1.getRole().level)
.map(DeskMember::fromModel)
.toList());
}
public Uni<List<VerySimpleMembre>> getMembers(SecurityCtx securityCtx) {
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
.invoke(Unchecked.consumer(m -> {
if (m == null || m.getClub() == null)
throw new DNotFoundException(trad.t("club.non.trouve"));
if (!securityCtx.isInClubGroup(m.getClub().getId()))
throw new DForbiddenException();
}))
.chain(m -> combRepository.list("club = ?1", m.getClub()))
.map(membreModels -> membreModels.stream()
.map(m -> new VerySimpleMembre(m.getLname(), m.getFname(), m.getLicence())).toList());
}
public Uni<String> updateOfUser(SecurityCtx securityCtx, PartClubForm form) {
TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() {
};
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
.invoke(Unchecked.consumer(m -> {
if (m == null || m.getClub() == null)
throw new DNotFoundException(trad.t("club.non.trouve"));
if (!securityCtx.isInClubGroup(m.getClub().getId()))
throw new DForbiddenException();
}))
.map(MembreModel::getClub)
.call(club -> Mutiny.fetch(club.getContact()))
.chain(Unchecked.function(club -> {
ls.logChange("Contact interne", club.getContact_intern(), form.getContact_intern(), club);
club.setContact_intern(form.getContact_intern());
ls.logChange("Adresse administrative", club.getAddress(), form.getAddress(), club);
club.setAddress(form.getAddress());
try {
if (!Objects.equals(club.getContact(), MAPPER.readValue(form.getContact(), typeRef)))
ls.logUpdate("Contact(s)...", club);
club.setContact(MAPPER.readValue(form.getContact(), typeRef));
} catch (JsonProcessingException e) {
throw new DBadRequestException(trad.t("erreur.de.format.des.contacts"));
}
ls.logChange("Lieux d'entrainements", club.getTraining_location(), form.getTraining_location(),
club);
club.setTraining_location(form.getTraining_location());
ls.logChange("Horaires d'entrainements", club.getTraining_day_time(), form.getTraining_day_time(),
club);
club.setTraining_day_time(form.getTraining_day_time());
return Panache.withTransaction(() -> repository.persist(club)).call(() -> ls.append());
}))
.map(__ -> "OK");
}
public Uni<String> update(long id, FullClubForm input) {
AtomicBoolean nameChange = new AtomicBoolean(false);
return repository.findById(id).call(m -> Mutiny.fetch(m.getContact()))
.onItem().transformToUni(Unchecked.function(m -> {
TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() {
};
if (!input.getName().equals(m.getName())) {
m.setName(input.getName());
nameChange.set(true);
}
m.setCountry(input.getCountry());
m.setInternational(input.isInternational());
if (!input.isInternational()) {
ls.logChange("Lieux d'entrainements", m.getTraining_location(), input.getTraining_location(),
m);
m.setTraining_location(input.getTraining_location());
ls.logChange("Horaires d'entrainements", m.getTraining_day_time(), input.getTraining_day_time(),
m);
m.setTraining_day_time(input.getTraining_day_time());
ls.logChange("Contact interne", m.getContact_intern(), input.getContact_intern(), m);
m.setContact_intern(input.getContact_intern());
if (input.getState_id() != null && !input.getState_id().isBlank()) {
ls.logChange("N° SIRET", m.getClubId(), input.getState_id(), m);
m.setStateId(input.getState_id());
}
ls.logChange("Adresse administrative", m.getAddress(), input.getAddress(), m);
m.setAddress(input.getAddress());
try {
if (!Objects.equals(m.getContact(), MAPPER.readValue(input.getContact(), typeRef)))
ls.logUpdate("Contact(s)...", m);
m.setContact(MAPPER.readValue(input.getContact(), typeRef));
} catch (JsonProcessingException e) {
throw new DBadRequestException(trad.t("erreur.de.format.des.contacts"));
}
}
return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append());
}))
.call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(
clubModel) // update group in keycloak
: Uni.createFrom().nullItem())
.invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients,
SimpleClubModel.fromModel(membreModel)))
.map(__ -> "OK");
}
public Uni<Long> add(FullClubForm input) {
TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() {
};
return Uni.createFrom().nullItem()
.chain(() -> {
ClubModel clubModel = new ClubModel();
clubModel.setName(input.getName());
clubModel.setCountry(input.getCountry());
clubModel.setInternational(input.isInternational());
clubModel.setNo_affiliation(null);
if (!input.isInternational()) {
clubModel.setTraining_location(input.getTraining_location());
clubModel.setTraining_day_time(input.getTraining_day_time());
clubModel.setContact_intern(input.getContact_intern());
if (input.getState_id() != null && !input.getState_id().isBlank())
clubModel.setStateId(input.getState_id());
clubModel.setAddress(input.getAddress());
try {
clubModel.setContact(MAPPER.readValue(input.getContact(), typeRef));
} catch (JsonProcessingException ignored) {
}
}
return Panache.withTransaction(() -> repository.persist(clubModel));
})
.call(clubModel -> ls.logAAdd(clubModel))
.call(clubModel -> keycloakService.getGroupFromClub(clubModel)) // create group in keycloak
.invoke(clubModel -> SReqClub.sendAddIfNeed(serverCustom.clients, SimpleClubModel.fromModel(clubModel)))
.map(ClubModel::getId);
}
public Uni<?> delete(long id) {
return repository.findById(id)
.chain(club -> combRepository.list("club = ?1", club)
.map(combModels -> combModels.stream().peek(combModel -> {
combModel.setClub(null);
combModel.setRole(RoleAsso.MEMBRE);
}).toList())
.call(list -> list.stream().noneMatch(m -> m.getUserId() != null) ?
Uni.createFrom().voidItem() :
Uni.join().all(list.stream().filter(m -> m.getUserId() != null)
.map(m -> keycloakService.clearUser(m.getUserId())).toList())
.andCollectFailures())
.chain(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
Panache.withTransaction(() -> combRepository.persist(list)))
.map(o -> club)
)
.call(clubModel -> (clubModel.getClubId() == null) ? Uni.createFrom()
.voidItem() : keycloakService.removeClubGroup(clubModel.getClubId()))
.invoke(membreModel -> SReqClub.sendRmIfNeed(serverCustom.clients, id))
.call(clubModel -> ls.logADelete(clubModel))
.chain(clubModel -> Panache.withTransaction(() -> repository.delete(clubModel)))
.call(__ -> Utils.deleteMedia(id, media, "ppClub"))
.call(__ -> Utils.deleteMedia(id, media, "clubStatus"));
}
public Uni<RenewAffData> getRenewData(long id, List<Long> mIds) {
RenewAffData data = new RenewAffData();
return repository.findById(id)
.call(clubModel -> Mutiny.fetch(clubModel.getAffiliations()))
.invoke(clubModel -> {
data.setName(clubModel.getName());
data.setState_id(clubModel.getStateId());
data.setAddress(clubModel.getAddress());
data.setContact(clubModel.getContact_intern());
data.setSaison(
clubModel.getAffiliations().stream().max(Comparator.comparing(AffiliationModel::getSaison))
.map(AffiliationModel::getSaison).map(i -> Math.min(i + 1, Utils.getSaison() + 1))
.orElse(Utils.getSaison()));
})
.chain(club -> combRepository.list("id IN ?1", mIds))
.invoke(combs -> data.setMembers(combs.stream()
.filter(o -> o.getRole() != null && o.getRole().level >= RoleAsso.MEMBREBUREAU.level)
.sorted((o1, o2) -> o2.getRole().level - o1.getRole().level)
.map(RenewAffData.RenewMember::new)
.toList()))
.map(o -> data);
}
public Uni<List<ClubMapData>> getMapData() {
return affiliationRepository.list("saison >= ?1 AND club.international = ?2", Utils.getSaison() - 1, false)
.toMulti().flatMap(list -> Multi.createFrom().iterable(list))
.call(affiliationModel -> Mutiny.fetch(affiliationModel.getClub().getContact()))
.map(affiliationModel -> {
ClubModel clubModel = affiliationModel.getClub();
ClubMapData data = new ClubMapData();
data.setName(clubModel.getName());
data.setUuid(clubModel.getClubId());
if (clubModel.getTraining_location() != null) {
try {
MAPPER.readTree(clubModel.getTraining_location()).forEach(l -> {
ClubMapData.Location loc = new ClubMapData.Location();
loc.setLat(l.get("lat").asDouble());
loc.setLng(l.get("lng").asDouble());
loc.setAddr(l.get("text").asText());
data.training_location.add(loc);
});
} catch (JsonProcessingException ignored) {
}
}
data.setTraining_day_time(clubModel.getTraining_day_time());
data.setContact(clubModel.getContact());
return data;
}).collect().asList();
}
}

View File

@@ -0,0 +1,318 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.CompetitionModel;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.data.repository.RegisterRepository;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleCompet;
import fr.titionfire.ffsaf.net2.request.SReqCompet;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import fr.titionfire.ffsaf.utils.RegisterMode;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ApplicationScoped
public class CompetPermService {
@Inject
ServerCustom serverCustom;
@Inject
CompetitionRepository competitionRepository;
@Inject
@CacheName("safca-config")
Cache cache;
@Inject
@CacheName("safca-have-access")
Cache cacheAccess;
@Inject
@CacheName("have-access")
Cache cacheNoneAccess;
@Inject
RegisterRepository registerRepository;
public Uni<SimpleCompet> getSafcaConfig(long id) {
return cache.get(id, k -> {
CompletableFuture<SimpleCompet> f = new CompletableFuture<>();
SReqCompet.getConfig(serverCustom.clients, id, f);
try {
return f.get(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
});
}
public Uni<List<Long>> getAllHaveAdminAccess(SecurityCtx securityCtx) {
ArrayList<Long> out = new ArrayList<>();
Uni<HashMap<Long, String>> safca = cacheAccess.getAsync(securityCtx.getSubject(),
k -> competitionRepository.list("system = ?1", CompetitionSystem.SAFCA)
.chain(competitionModels -> {
CompletableFuture<HashMap<String, String>> f = new CompletableFuture<>();
SReqCompet.getAllHaveAccess(serverCustom.clients, securityCtx.getSubject(), f);
return Uni.createFrom().future(f, Duration.ofMillis(500))
.onFailure().recoverWithItem(new HashMap<>())
.map(map_ -> {
HashMap<Long, String> map = new HashMap<>();
map_.forEach((key, value) -> map.put(Long.parseLong(key), value));
for (CompetitionModel model : competitionModels) {
if (model.getOwner().equals(securityCtx.getSubject()))
map.putIfAbsent(model.getId(), "owner");
else if (securityCtx.roleHas("federation_admin")
|| securityCtx.roleHas("safca_super_admin"))
map.putIfAbsent(model.getId(), "admin");
}
return map;
});
}))
.onFailure().call(throwable -> cacheAccess.invalidate(securityCtx.getSubject()));
Uni<HashMap<Long, String>> none = cacheNoneAccess.getAsync(securityCtx.getSubject(),
k -> competitionRepository.list("system = ?1", CompetitionSystem.INTERNAL)
.map(competitionModels -> {
HashMap<Long, String> map = new HashMap<>();
for (CompetitionModel model : competitionModels) {
if (model.getOwner().equals(securityCtx.getSubject()))
map.putIfAbsent(model.getId(), "owner");
else if (securityCtx.roleHas("federation_admin"))
map.putIfAbsent(model.getId(), "admin");
else if (securityCtx.isInClubGroup(
model.getClub().getId()) && (securityCtx.isClubAdmin()))
map.putIfAbsent(model.getId(), "admin");
else if (model.getAdmin().contains(securityCtx.getSubject()))
map.putIfAbsent(model.getId(), "admin");
else if (model.getTable().contains(securityCtx.getSubject()))
map.putIfAbsent(model.getId(), "table");
}
return map;
}));
return safca.invoke(map ->
map.forEach((k, v) -> {
if (v.equals("owner") || v.equals("admin"))
out.add(k);
})
)
.call(__ -> none.invoke(map ->
map.forEach((k, v) -> {
if (v.equals("owner") || v.equals("admin"))
out.add(k);
})
))
.map(__ -> out.stream().distinct().toList());
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has view perm
*/
public Uni<CompetitionModel> hasViewPerm(SecurityCtx securityCtx, CompetitionModel competitionModel) {
return hasViewPerm(securityCtx, Uni.createFrom().item(competitionModel));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has view perm
*/
public Uni<CompetitionModel> hasViewPerm(SecurityCtx securityCtx, long id) {
return hasViewPerm(securityCtx, competitionRepository.findById(id));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has view perm
*/
public Uni<CompetitionModel> hasViewPerm(SecurityCtx securityCtx, Uni<CompetitionModel> in) {
return in.call(cm -> (cm.isPublicVisible() || cm.getRegisterMode() == RegisterMode.FREE
|| cm.getRegisterMode() == RegisterMode.HELLOASSO
|| (cm.getRegisterMode() == RegisterMode.CLUB_ADMIN && securityCtx.isClubAdmin())) ?
Uni.createFrom().nullItem() :
hasAdminViewPerm(securityCtx, cm).onFailure()
.recoverWithUni(__ ->
registerRepository.count("membre.userId = ?1 AND competition = ?2",
securityCtx.getSubject(), cm).map(Unchecked.function(c -> {
if (c == 0)
throw new DForbiddenException();
return cm;
}))
));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has admin view perm
*/
public Uni<CompetitionModel> hasAdminViewPerm(SecurityCtx securityCtx, CompetitionModel competitionModel) {
return hasAdminViewPerm(securityCtx, Uni.createFrom().item(competitionModel));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has admin view perm
*/
public Uni<CompetitionModel> hasAdminViewPerm(SecurityCtx securityCtx, long id) {
return hasAdminViewPerm(securityCtx, competitionRepository.findById(id));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has admin view perm
*/
public Uni<CompetitionModel> hasAdminViewPerm(SecurityCtx securityCtx, Uni<CompetitionModel> in) {
return in.call(Unchecked.function(o -> {
if (securityCtx.getSubject().equals(o.getOwner()) || securityCtx.roleHas("federation_admin"))
return Uni.createFrom().nullItem();
if (o.getSystem() == CompetitionSystem.SAFCA)
return hasSafcaViewPerm(securityCtx, o.getId());
if (o.getAdmin().contains(securityCtx.getSubject()))
return Uni.createFrom().nullItem();
if (!securityCtx.isInClubGroup(o.getClub().getId())) // Only membre club pass here
throw new DForbiddenException();
if (o.getSystem() == CompetitionSystem.INTERNAL)
if (securityCtx.isClubAdmin())
return Uni.createFrom().nullItem();
throw new DForbiddenException();
})
);
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasEditPerm(SecurityCtx securityCtx, CompetitionModel competitionModel) {
return hasEditPerm(securityCtx, Uni.createFrom().item(competitionModel));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasEditPerm(SecurityCtx securityCtx, long id) {
return hasEditPerm(securityCtx, competitionRepository.findById(id));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasEditPerm(SecurityCtx securityCtx, Uni<CompetitionModel> in) {
return in.call(Unchecked.function(o -> {
if (securityCtx.getSubject().equals(o.getOwner()) || securityCtx.roleHas("federation_admin"))
return Uni.createFrom().nullItem();
if (o.getSystem() == CompetitionSystem.SAFCA)
return hasSafcaEditPerm(securityCtx, o.getId());
if (o.getSystem() == CompetitionSystem.INTERNAL) {
if (securityCtx.isInClubGroup(o.getClub().getId()) && securityCtx.isClubAdmin())
return Uni.createFrom().nullItem();
if (o.getAdmin().contains(securityCtx.getSubject()))
return Uni.createFrom().nullItem();
throw new DForbiddenException();
}
throw new DForbiddenException();
})
);
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasTablePerm(SecurityCtx securityCtx, CompetitionModel competitionModel) {
return hasTablePerm(securityCtx, Uni.createFrom().item(competitionModel));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasTablePerm(SecurityCtx securityCtx, long id) {
return hasTablePerm(securityCtx, competitionRepository.findById(id));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasTablePerm(SecurityCtx securityCtx, Uni<CompetitionModel> in) {
return in.call(Unchecked.function(o -> {
if (securityCtx.getSubject().equals(o.getOwner()) || securityCtx.roleHas("federation_admin"))
return Uni.createFrom().nullItem();
if (o.getSystem() == CompetitionSystem.SAFCA)
return hasSafcaTablePerm(securityCtx, o.getId());
if (o.getSystem() == CompetitionSystem.INTERNAL) {
if (securityCtx.isInClubGroup(o.getClub().getId()) && securityCtx.isClubAdmin())
return Uni.createFrom().nullItem();
if (o.getAdmin().contains(securityCtx.getSubject()))
return Uni.createFrom().nullItem();
if (o.getTable().contains(securityCtx.getSubject()))
return Uni.createFrom().nullItem();
throw new DForbiddenException();
}
throw new DForbiddenException();
})
);
}
private Uni<?> hasSafcaViewPerm(SecurityCtx securityCtx, long id) {
return securityCtx.roleHas("safca_super_admin") ?
Uni.createFrom().nullItem()
:
getSafcaConfig(id).chain(Unchecked.function(o -> {
if (!o.admin().contains(UUID.fromString(securityCtx.getSubject()))
&& !o.table().contains(UUID.fromString(securityCtx.getSubject())))
throw new DForbiddenException();
return Uni.createFrom().nullItem();
}));
}
private Uni<?> hasSafcaEditPerm(SecurityCtx securityCtx, long id) {
return securityCtx.roleHas("safca_super_admin") ?
Uni.createFrom().nullItem()
:
getSafcaConfig(id).chain(Unchecked.function(o -> {
if (!o.admin().contains(UUID.fromString(securityCtx.getSubject())))
throw new DForbiddenException();
return Uni.createFrom().nullItem();
}));
}
private Uni<?> hasSafcaTablePerm(SecurityCtx securityCtx, long id) {
return securityCtx.roleHas("safca_super_admin") ?
Uni.createFrom().nullItem()
:
getSafcaConfig(id).chain(Unchecked.function(o -> {
if (!o.admin().contains(UUID.fromString(securityCtx.getSubject()))
&& !o.table().contains(UUID.fromString(securityCtx.getSubject())))
throw new DForbiddenException();
return Uni.createFrom().nullItem();
}));
}
}

View File

@@ -0,0 +1,968 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleCompet;
import fr.titionfire.ffsaf.net2.request.SReqCompet;
import fr.titionfire.ffsaf.net2.request.SReqRegister;
import fr.titionfire.ffsaf.rest.client.dto.NotificationData;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.*;
import fr.titionfire.ffsaf.ws.send.SRegister;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.reactive.ReactiveMailer;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import io.vertx.mutiny.core.Vertx;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.*;
import java.util.stream.Stream;
@WithSession
@ApplicationScoped
public class CompetitionService {
private static final Logger LOGGER = Logger.getLogger(CompetitionService.class);
@Inject
CompetitionRepository repository;
@Inject
CategoryRepository categoryRepository;
@Inject
MatchRepository matchRepository;
@Inject
RegisterRepository registerRepository;
@Inject
KeycloakService keycloakService;
@Inject
CombRepository combRepository;
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
CatPresetRepository catPresetRepository;
@Inject
ServerCustom serverCustom;
@Inject
MembreService membreService;
@Inject
CompetPermService permService;
@Inject
HelloAssoRegisterRepository helloAssoRepository;
@SuppressWarnings("CdiInjectionPointsInspection")
@Inject
ReactiveMailer reactiveMailer;
@Inject
Vertx vertx;
@Inject
SRegister sRegister;
@Inject
@CacheName("safca-config")
Cache cache;
@Inject
@CacheName("safca-have-access")
Cache cacheAccess;
@Inject
@CacheName("have-access")
Cache cacheNoneAccess;
@Inject
TradService trad;
public Uni<CompetitionData> getById(SecurityCtx securityCtx, Long id) {
return permService.hasViewPerm(securityCtx, id).map(cm -> {
CompetitionData out = CompetitionData.fromModelLight(cm);
if (cm.getAdmin() != null) {
out.setCanEditRegisters(cm.getAdmin().stream().anyMatch(u -> u.equals(securityCtx.getSubject())));
}
return out;
});
}
public Uni<CompetitionData> getByIdAdmin(SecurityCtx securityCtx, Long id) {
if (id == 0) {
return Uni.createFrom().item(new CompetitionData());
}
return permService.hasAdminViewPerm(securityCtx, id)
.call(competitionModel -> Mutiny.fetch(competitionModel.getCatPreset()))
.chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc())
.chain(insc -> Mutiny.fetch(competitionModel.getGuests())
.map(guest -> CompetitionData.fromModel(competitionModel).addInsc(insc, guest)
.addPresets(competitionModel.getCatPreset()))))
.chain(data ->
vertx.getOrCreateContext().executeBlocking(() -> {
keycloakService.getUser(UUID.fromString(data.getOwner()))
.ifPresent(user -> data.setOwner(user.getUsername()));
return data;
})
);
}
public Uni<List<CompetitionData>> getAll(SecurityCtx securityCtx) {
List<CompetitionData> out = new ArrayList<>();
return permService.getAllHaveAdminAccess(securityCtx)
.call(ids -> repository.list("id IN ?1", ids)
.invoke(cm -> {
out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
out.forEach(competition -> competition.setCanEdit(true));
}))
.chain(ids -> repository.list("id NOT IN ?1 AND ?2 IN admin", ids, securityCtx.getSubject())
.map(cm -> {
out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
out.forEach(competition -> competition.setCanEditRegisters(true));
List<Long> ids2 = new ArrayList<>(ids);
ids2.addAll(cm.stream().map(CompetitionModel::getId).toList());
return ids2;
}))
.call(ids ->
repository.list("id NOT IN ?1 AND (publicVisible = TRUE OR registerMode IN ?2)", ids,
securityCtx.isClubAdmin() ? List.of(RegisterMode.FREE, RegisterMode.HELLOASSO,
RegisterMode.CLUB_ADMIN) : List.of(RegisterMode.FREE, RegisterMode.HELLOASSO))
.invoke(cm -> out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList()))
.call(cm -> registerRepository.list(
"membre.userId = ?1 AND competition.id NOT IN ?2 AND competition NOT IN ?3",
securityCtx.getSubject(), ids, cm)
.chain(registerModels -> {
Uni<Void> uni = Uni.createFrom().nullItem();
for (RegisterModel registerModel : registerModels) {
uni = uni.call(__ -> Mutiny.fetch(registerModel.getCompetition())
.invoke(cm2 -> out.add(CompetitionData.fromModelLight(cm2))));
}
return uni;
})
))
.map(__ -> out);
}
public Uni<List<CompetitionData>> getAllAdmin(SecurityCtx securityCtx) {
return permService.getAllHaveAdminAccess(securityCtx)
.chain(ids -> repository.list("id IN ?1", ids))
.map(pouleModels -> pouleModels.stream().map(CompetitionData::fromModel).toList());
}
public Uni<List<CompetitionData>> getAllSystemAdmin(SecurityCtx securityCtx,
CompetitionSystem system) {
return permService.getAllHaveAdminAccess(securityCtx)
.chain(ids -> repository.list("system = ?1 AND id IN ?2", system, ids))
.map(pouleModels -> pouleModels.stream().map(CompetitionData::fromModel).toList());
}
public Uni<List<CompetitionData>> getAllSystemTable(SecurityCtx securityCtx,
CompetitionSystem system) {
return repository.list("system = ?1", system)
.chain(l -> Uni.join().all(l.stream().map(cm ->
permService.hasTablePerm(securityCtx, cm).onFailure().recoverWithNull()
).toList()).andCollectFailures())
.map(l -> l.stream().filter(Objects::nonNull).map(CompetitionData::fromModel).toList());
}
public Uni<CompetitionData> addOrUpdate(SecurityCtx securityCtx, CompetitionData data) {
if (data.getId() == null) {
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
.invoke(Unchecked.consumer(combModel -> {
if (!securityCtx.getRoles().contains("create_compet") && !securityCtx.getRoles()
.contains("federation_admin"))
throw new DForbiddenException(trad.t("vous.ne.pouvez.pas.creer.de.competition"));
}))
.map(MembreModel::getClub)
.chain(clubModel -> {
CompetitionModel model = new CompetitionModel();
model.setId(null);
model.setSystem(data.getSystem());
model.setClub(clubModel);
model.setInsc(new ArrayList<>());
model.setGuests(new ArrayList<>());
model.setUuid(UUID.randomUUID().toString());
model.setOwner(securityCtx.getSubject());
model.setCatPreset(new ArrayList<>());
copyData(data, model);
return Panache.withTransaction(() -> repository.persist(model));
})
.call(model -> syncPreset(data, model))
.map(CompetitionData::fromModel)
.call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate(
securityCtx.getSubject()) : Uni.createFrom().nullItem())
.call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate(
securityCtx.getSubject()) : Uni.createFrom().nullItem());
} else {
return permService.hasEditPerm(securityCtx, data.getId())
.call(model -> Mutiny.fetch(model.getCatPreset()))
.chain(model -> {
copyData(data, model);
return vertx.getOrCreateContext().executeBlocking(() -> // Update owner
keycloakService.getUser(data.getOwner()).map(UserRepresentation::getId).orElse(null))
.invoke(Unchecked.consumer(newOwner -> {
if (newOwner == null)
throw new DBadRequestException(
String.format(trad.t("user.not.found"), data.getOwner()));
if (!newOwner.equals(model.getOwner())) {
if (!securityCtx.roleHas("federation_admin")
&& !securityCtx.roleHas("safca_super_admin")
&& !securityCtx.getSubject().equals(model.getOwner()))
throw new DForbiddenException();
model.setOwner(newOwner);
}
}))
.chain(__ -> Panache.withTransaction(() -> repository.persist(model)));
})
.call(model -> syncPreset(data, model))
.map(model -> CompetitionData.fromModel(model).addPresets(model.getCatPreset()))
.call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate(
securityCtx.getSubject()) : Uni.createFrom().nullItem())
.call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate(
securityCtx.getSubject()) : Uni.createFrom().nullItem());
}
}
private Uni<?> syncPreset(CompetitionData data, CompetitionModel model) {
List<Long> toRemoveId = model.getCatPreset().stream()
.map(CatPresetModel::getId)
.filter(id -> data.getPresets().stream().noneMatch(preset -> Objects.equals(preset.getId(), id)))
.toList();
for (PresetData preset : data.getPresets()) {
CatPresetModel presetModel;
if (preset.getId() != null && preset.getId() > 0) {
presetModel = model.getCatPreset().stream()
.filter(p -> p.getId().equals(preset.getId()))
.findFirst()
.orElse(new CatPresetModel());
} else {
presetModel = new CatPresetModel();
model.getCatPreset().add(presetModel);
}
presetModel.setCompetition(model);
presetModel.setName(preset.getName());
presetModel.setSwordType(preset.getSword());
presetModel.setShieldType(preset.getShield());
presetModel.setCategories(preset.getCategories());
presetModel.setMandatoryProtection1(preset.getMandatoryProtection1());
presetModel.setMandatoryProtection2(preset.getMandatoryProtection2());
}
// Remove deleted presets
model.getCatPreset().removeIf(presetModel -> toRemoveId.contains(presetModel.getId()));
return Panache.withTransaction(() -> repository.persist(model)
.call(__ -> {
if (!toRemoveId.isEmpty()) {
return catPresetRepository.delete("id IN ?1", toRemoveId);
}
return Uni.createFrom().nullItem();
}));
}
private void copyData(CompetitionData data, CompetitionModel model) {
if (model.getBanMembre() == null)
model.setBanMembre(new ArrayList<>());
model.setName(data.getName());
model.setAdresse(data.getAdresse());
model.setDescription(data.getDescription());
model.setDate(data.getDate());
model.setTodate(data.getDate());
model.setPublicVisible(data.isPublicVisible());
model.setStartRegister(data.getStartRegister());
model.setEndRegister(data.getEndRegister());
model.setRegisterMode(data.getRegisterMode());
model.setRequiredWeight(data.getRequiredWeight());
model.setData1(data.getData1());
model.setData2(data.getData2());
model.setData3(data.getData3());
model.setData4(data.getData4());
}
public Uni<List<SimpleRegisterComb>> getRegister(SecurityCtx securityCtx, Long id, String source) {
if ("admin".equals(source))
return permService.hasEditPerm(securityCtx, id)
.chain(c -> {
Uni<List<SimpleRegisterComb>> uni = Mutiny.fetch(c.getInsc())
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences()))
.onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites()))
.map(cm -> SimpleRegisterComb.fromModel(cm, cm.getMembre().getLicences())
.setCategorieInscrite(cm.getCategoriesInscrites()))
.collect().asList();
return uni
.call(l -> Mutiny.fetch(c.getGuests())
.onItem().transformToMulti(Multi.createFrom()::iterable)
.filter(g -> !g.isTeam())
.onItem().call(guest -> Mutiny.fetch(guest.getCategoriesInscrites()))
.map(guest -> SimpleRegisterComb.fromModel(guest)
.setCategorieInscrite(guest.getCategoriesInscrites()))
.collect().asList()
.invoke(l::addAll));
});
if ("club".equals(source))
return Uni.createFrom().nullItem()
.invoke(Unchecked.consumer(__ -> {
if (!securityCtx.isClubAdmin())
throw new DForbiddenException();
}))
.chain(__ -> membreService.getByAccountId(securityCtx.getSubject()))
.chain(model -> registerRepository.list("competition.id = ?1 AND membre.club = ?2", id,
model.getClub()))
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences()))
.onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites()))
.map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences())
.setCategorieInscrite(combModel.getCategoriesInscrites()))
.collect().asList();
return membreService.getByAccountId(securityCtx.getSubject())
.chain(model -> registerRepository.find("competition.id = ?1 AND membre = ?2", id, model).firstResult()
.call(rm -> rm == null ? Uni.createFrom().voidItem() :
Mutiny.fetch(rm.getCategoriesInscrites()))
.map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of())
.setCategorieInscrite(rm.getCategoriesInscrites()))));
}
public Uni<SimpleRegisterComb> addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data,
String source) {
if ("admin".equals(source))
if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
return permService.hasEditPerm(securityCtx, id)
.chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
.call(combModel -> {
if (c.getBanMembre() == null)
c.setBanMembre(new ArrayList<>());
c.getBanMembre().remove(combModel.getId());
return Panache.withTransaction(() -> repository.persist(c));
})
.chain(combModel -> updateRegister(data, c, combModel, true, false)))
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom()
.voidItem())
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites()));
} else {
return permService.hasEditPerm(securityCtx, id)
.chain(c -> competitionGuestRepository.findById(data.getId() * -1)
.map(g -> {
if (g != null)
return g;
CompetitionGuestModel model = new CompetitionGuestModel();
model.setCompetition(c);
return model;
}))
.invoke(model -> {
model.setFname(data.getFname());
if (data.getLname().equals("__team"))
model.setLname("_team");
else
model.setLname(data.getLname());
model.setGenre(data.getGenre());
model.setClub(data.getClub());
model.setCountry(data.getCountry());
model.setWeightReal(data.getWeightReal());
model.setCategorie(data.getCategorie());
if (model.getCompetition().getRequiredWeight().contains(model.getCategorie()))
model.setWeight(data.getWeight());
})
.call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
.call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(),
data.getCategoriesInscrites())
.invoke(cats -> {
g.getCategoriesInscrites().clear();
g.getCategoriesInscrites().addAll(cats);
g.getCategoriesInscrites()
.removeIf(cat -> cat.getCategories().stream()
.noneMatch(e -> e.getCategorie().equals(g.getCategorie())));
}))
.chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
.call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterNoFetch(model.getCompetition().getUuid(), r)
: Uni.createFrom().voidItem()))
.map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
}
if ("club".equals(source))
return repository.findById(id)
.invoke(Unchecked.consumer(cm -> {
if (!(cm.getRegisterMode() == RegisterMode.CLUB_ADMIN || cm.getRegisterMode() == RegisterMode.FREE)
|| !securityCtx.isClubAdmin())
throw new DForbiddenException();
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
throw new DBadRequestException(trad.t("inscription.fermee"));
}))
.chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
.invoke(Unchecked.consumer(model -> {
if (!securityCtx.isInClubGroup(model.getClub().getId()))
throw new DForbiddenException();
if (c.getBanMembre().contains(model.getId()))
throw new DForbiddenException(trad.t("insc.err1"));
}))
.chain(combModel -> updateRegister(data, c, combModel, false, false)))
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom()
.voidItem())
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites()));
return repository.findById(id)
.invoke(Unchecked.consumer(cm -> {
if (cm.getRegisterMode() != RegisterMode.FREE)
throw new DForbiddenException();
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
throw new DBadRequestException(trad.t("inscription.fermee"));
}))
.chain(c -> membreService.getByAccountId(securityCtx.getSubject())
.invoke(Unchecked.consumer(model -> {
if (c.getBanMembre().contains(model.getId()))
throw new DForbiddenException(trad.t("insc.err2"));
}))
.chain(combModel -> updateRegister(data, c, combModel, false, false)))
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom().voidItem())
.map(r -> SimpleRegisterComb.fromModel(r, List.of()).setCategorieInscrite(r.getCategoriesInscrites()));
}
public Uni<List<SimpleRegisterComb>> addRegistersComb(SecurityCtx securityCtx, Long id,
List<RegisterRequestData> datas,
String source) {
if (!"admin".equals(source))
return Uni.createFrom().failure(new DForbiddenException());
return permService.hasEditPerm(securityCtx, id)
.chain(cm -> Multi.createFrom().iterable(datas).onItem().transformToUni(data ->
makeImportUpdate(cm, data).onFailure().recoverWithItem(t -> {
SimpleRegisterComb errorComb = new SimpleRegisterComb();
errorComb.setLicence(-42);
errorComb.setFname("ERROR");
errorComb.setLname(t.getMessage());
return errorComb;
})).concatenate().collect().asList());
}
@WithSession
public Uni<SimpleRegisterComb> makeImportUpdate(CompetitionModel c, RegisterRequestData data) {
if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
return findComb(data.getLicence(), data.getFname(), data.getLname())
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
.call(combModel -> {
if (c.getBanMembre() == null)
c.setBanMembre(new ArrayList<>());
c.getBanMembre().remove(combModel.getId());
return Panache.withTransaction(() -> repository.persist(c));
})
.chain(combModel -> updateRegister(data, c, combModel, true, true))
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites()));
} else {
return findGuestOrInit(data.getFname(), data.getLname(), c)
.invoke(Unchecked.consumer(model -> {
if (data.getCategorie() == null)
throw new DBadRequestException(trad.t("categorie.requise"));
model.setCategorie(data.getCategorie());
if (data.getGenre() == null) {
if (model.getGenre() == null)
data.setGenre(Genre.NA);
} else
model.setGenre(data.getGenre());
if (data.getClub() == null) {
if (model.getClub() == null)
data.setClub("");
} else
model.setClub(data.getClub());
if (data.getCountry() == null) {
if (model.getCountry() == null)
data.setCountry("FR");
} else
model.setCountry(data.getCountry());
if (c.getRequiredWeight().contains(model.getCategorie())) {
if (data.getCountry() != null)
model.setWeight(data.getWeight());
}
}))
.call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
.call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", c,
data.getCategoriesInscrites())
.invoke(cats -> {
g.getCategoriesInscrites().clear();
g.getCategoriesInscrites().addAll(cats);
g.getCategoriesInscrites().removeIf(cat -> cat.getCategories().stream()
.noneMatch(e -> e.getCategorie().equals(g.getCategorie())));
}))
.chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem()))
.map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
}
}
private Uni<RegisterModel> updateRegister(RegisterRequestData data, CompetitionModel c,
MembreModel combModel, boolean admin, boolean append) {
return registerRepository.find("competition = ?1 AND membre = ?2", c, combModel).firstResult()
.onFailure().recoverWithNull()
.map(Unchecked.function(r -> {
if (r != null) {
if (!admin && r.isLockEdit())
throw new DForbiddenException(trad.t("insc.err3"));
if (data.getOverCategory() != null || !append)
if (data.getOverCategory() == null)
r.setOverCategory(0);
else
r.setOverCategory(data.getOverCategory());
r.setCategorie(
(combModel.getBirth_date() == null) ? combModel.getCategorie() :
Utils.getCategoryFormBirthDate(combModel.getBirth_date(),
c.getDate()));
int days = Utils.getDaysBeforeCompetition(c.getDate());
if (days > -7)
r.setClub(combModel.getClub());
if (c.getRequiredWeight().contains(r.getCategorie2()))
if (data.getCountry() != null || !append)
r.setWeight(data.getWeight());
if (admin) {
r.setWeightReal(data.getWeightReal());
r.setLockEdit(data.isLockEdit());
}
} else {
r = new RegisterModel(c, combModel, data.getWeight(), data.getOverCategory(),
(combModel.getBirth_date() == null) ? combModel.getCategorie() :
Utils.getCategoryFormBirthDate(combModel.getBirth_date(),
c.getDate()),
(combModel.getClub() == null) ? null : combModel.getClub());
if (admin) {
r.setWeightReal(data.getWeightReal());
r.setLockEdit(data.isLockEdit());
} else
r.setLockEdit(false);
}
if (c.getSystem() == CompetitionSystem.SAFCA) {
SReqRegister.sendIfNeed(serverCustom.clients,
new CompetitionData.SimpleRegister(r.getMembre().getId(),
r.getOverCategory(), r.getWeight2(), r.getCategorie(),
(r.getClub() == null) ? null : r.getClub().getId(),
(r.getClub() == null) ? null : r.getClub().getName()), c.getId());
}
return r;
}))
.call(r -> Mutiny.fetch(r.getCategoriesInscrites()).chain(__ ->
catPresetRepository.list("competition = ?1 AND id IN ?2", c, data.getCategoriesInscrites())
.invoke(cats -> {
if (data.isQuick()) {
cats.removeIf(cat -> r.getCategoriesInscrites().stream()
.anyMatch(cp -> cp.equals(cat)));
} else {
r.getCategoriesInscrites().clear();
}
r.getCategoriesInscrites().addAll(cats);
r.getCategoriesInscrites()
.removeIf(cat -> cat.getCategories().stream()
.noneMatch(e -> e.getCategorie().equals(r.getCategorie2())));
})))
.chain(r -> Panache.withTransaction(() -> registerRepository.persist(r)))
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem());
}
private Uni<CompetitionGuestModel> findGuestOrInit(String fname, String lname, CompetitionModel competition) {
if (fname == null || lname == null)
return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis")));
return competitionGuestRepository.find(
"unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2) AND competition = ?3",
lname, fname, competition).firstResult()
.map(guestModel -> {
if (guestModel == null) {
CompetitionGuestModel model = new CompetitionGuestModel();
model.setFname(fname);
if (lname.equals("__team"))
model.setLname("_team");
else
model.setLname(lname);
model.setCompetition(competition);
return model;
}
return guestModel;
});
}
private Uni<MembreModel> findComb(Long licence, String fname, String lname) {
if (licence != null && licence > 0) {
return combRepository.find("licence = ?1", licence).firstResult()
.invoke(Unchecked.consumer(combModel -> {
if (combModel == null)
throw new DForbiddenException(String.format(trad.t("licence.non.trouve"), licence));
}));
} else {
if (fname == null || lname == null)
return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis")));
return combRepository.find("unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2)",
lname,
fname).firstResult()
.invoke(Unchecked.consumer(combModel -> {
if (combModel == null)
throw new DForbiddenException(String.format(trad.t("combattant.non.trouve"), fname, lname));
}));
}
}
public Uni<Void> removeRegisterComb(SecurityCtx securityCtx, Long id, Long combId, String source, boolean ban) {
if ("admin".equals(source))
return permService.hasEditPerm(securityCtx, id)
.chain(cm -> {
if (cm.getBanMembre() == null)
cm.setBanMembre(new ArrayList<>());
if (ban) {
if (!cm.getBanMembre().contains(combId))
cm.getBanMembre().add(combId);
} else {
cm.getBanMembre().remove(combId);
}
return Panache.withTransaction(() -> repository.persist(cm));
})
.chain(c -> deleteRegister(combId, c, true));
if ("club".equals(source))
return repository.findById(id)
.invoke(Unchecked.consumer(cm -> {
if (!(cm.getRegisterMode() == RegisterMode.CLUB_ADMIN || cm.getRegisterMode() == RegisterMode.FREE)
|| !securityCtx.isClubAdmin())
throw new DForbiddenException();
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
throw new DBadRequestException(trad.t("inscription.fermee"));
}))
.call(cm -> membreService.getById(combId)
.invoke(Unchecked.consumer(model -> {
if (model == null)
throw new DNotFoundException(
String.format(trad.t("le.membre.n.existe.pas"), combId));
if (!securityCtx.isInClubGroup(model.getClub().getId()))
throw new DForbiddenException();
})))
.chain(c -> deleteRegister(combId, c, false));
return repository.findById(id)
.call(cm -> membreService.getByAccountId(securityCtx.getSubject())
.invoke(Unchecked.consumer(model -> {
if (cm.getRegisterMode() != RegisterMode.FREE || !Objects.equals(model.getId(), combId))
throw new DForbiddenException();
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
throw new DBadRequestException(trad.t("inscription.fermee"));
})))
.chain(c -> deleteRegister(combId, c, false));
}
private Uni<Void> deleteRegister(Long combId, CompetitionModel c, boolean admin) {
if (admin && combId < 0) {
return competitionGuestRepository.find("competition = ?1 AND id = ?2", c, combId * -1).firstResult()
.onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit")))
.call(Unchecked.function(
model -> Panache.withTransaction(() -> competitionGuestRepository.delete(model))
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterRemove(c.getUuid(), combId) : Uni.createFrom()
.voidItem())))
.replaceWithVoid();
}
return registerRepository.find("competition = ?1 AND membre.id = ?2", c, combId).firstResult()
.onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit")))
.call(Unchecked.function(registerModel -> {
if (!admin && registerModel.isLockEdit())
throw new DForbiddenException(trad.t("insc.err3"));
return Panache.withTransaction(() -> registerRepository.delete(registerModel))
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterRemove(c.getUuid(), combId) : Uni.createFrom().voidItem());
}))
.replaceWithVoid();
}
public Uni<?> delete(SecurityCtx securityCtx, Long id) {
return repository.findById(id).invoke(Unchecked.consumer(c -> {
if (!(securityCtx.getSubject().equals(c.getOwner()) || securityCtx.roleHas("federation_admin")))
throw new DForbiddenException();
}))
.call(competitionModel -> categoryRepository.list("compet = ?1", competitionModel)
.call(pouleModels -> pouleModels.isEmpty() ? Uni.createFrom().nullItem() :
Uni.join().all(pouleModels.stream()
.map(pouleModel -> Panache.withTransaction(
() -> matchRepository.delete("poule = ?1", pouleModel.getId())))
.toList())
.andCollectFailures()))
.call(competitionModel -> Panache.withTransaction(
() -> categoryRepository.delete("compet = ?1", competitionModel)))
.call(competitionModel -> Panache.withTransaction(
() -> registerRepository.delete("competition = ?1", competitionModel)))
.call(competitionModel -> Panache.withTransaction(
() -> competitionGuestRepository.delete("competition = ?1", competitionModel)))
.chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId())))
.invoke(o -> SReqCompet.rmCompet(serverCustom.clients, id))
.call(__ -> cache.invalidate(id));
}
public Uni<SimpleCompetData> getInternalData(SecurityCtx securityCtx, Long id) {
return permService.hasEditPerm(securityCtx, id)
.invoke(Unchecked.consumer(cm -> {
if (cm.getSystem() != CompetitionSystem.INTERNAL)
throw new DBadRequestException(trad.t("competition.is.not.internal"));
}))
.chain(competitionModel -> {
SimpleCompetData data = SimpleCompetData.fromModel(competitionModel);
return vertx.getOrCreateContext().executeBlocking(() -> {
if (competitionModel.getAdmin() != null)
data.setAdmin(
competitionModel.getAdmin().stream().map(uuid -> keycloakService.getUserById(uuid))
.filter(Optional::isPresent)
.map(user -> user.get().getUsername())
.toList());
if (competitionModel.getTable() != null)
data.setTable(
competitionModel.getTable().stream().map(uuid -> keycloakService.getUserById(uuid))
.filter(Optional::isPresent)
.map(user -> user.get().getUsername())
.toList());
return data;
});
});
}
public Uni<Void> setInternalData(SecurityCtx securityCtx, SimpleCompetData data) {
return permService.hasEditPerm(securityCtx, data.getId())
.invoke(Unchecked.consumer(cm -> {
if (cm.getSystem() != CompetitionSystem.INTERNAL)
throw new DBadRequestException(trad.t("competition.is.not.internal"));
}))
.chain(cm -> vertx.getOrCreateContext().executeBlocking(() -> {
ArrayList<String> admin = new ArrayList<>();
ArrayList<String> table = new ArrayList<>();
for (String username : data.getAdmin()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
admin.add(opt.get().getId());
}
for (String username : data.getTable()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
table.add(opt.get().getId());
}
cm.setAdmin(admin);
cm.setTable(table);
cm.setConfig(data.getConfigForInternal());
return cm;
}))
.chain(cm -> Panache.withTransaction(() -> repository.persist(cm)))
.replaceWithVoid();
}
public Uni<SimpleCompetData> getSafcaData(SecurityCtx securityCtx, Long id) {
return permService.getSafcaConfig(id)
.call(Unchecked.function(o -> {
if (!securityCtx.getSubject().equals(o.owner())
&& !securityCtx.roleHas("federation_admin")
&& !securityCtx.roleHas("safca_super_admin")
&& !o.admin().contains(UUID.fromString(securityCtx.getSubject()))
&& !o.table().contains(UUID.fromString(securityCtx.getSubject())))
throw new DForbiddenException();
return Uni.createFrom().nullItem();
}))
.chain(simpleCompet -> {
SimpleCompetData data = SimpleCompetData.fromModel(simpleCompet);
return vertx.getOrCreateContext().executeBlocking(() -> {
data.setAdmin(simpleCompet.admin().stream().map(uuid -> keycloakService.getUser(uuid))
.filter(Optional::isPresent)
.map(user -> user.get().getUsername())
.toList());
data.setTable(simpleCompet.table().stream().map(uuid -> keycloakService.getUser(uuid))
.filter(Optional::isPresent)
.map(user -> user.get().getUsername())
.toList());
return data;
});
});
}
public Uni<?> setSafcaData(SecurityCtx securityCtx, SimpleCompetData data) {
return permService.hasEditPerm(securityCtx, data.getId())
.chain(__ -> vertx.getOrCreateContext().executeBlocking(() -> {
ArrayList<UUID> admin = new ArrayList<>();
ArrayList<UUID> table = new ArrayList<>();
for (String username : data.getAdmin()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
admin.add(UUID.fromString(opt.get().getId()));
}
for (String username : data.getTable()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
table.add(UUID.fromString(opt.get().getId()));
}
return new SimpleCompet(data.getId(), "", data.isShow_blason(),
data.isShow_flag(), admin, table);
}))
.invoke(simpleCompet -> SReqCompet.sendUpdate(serverCustom.clients, simpleCompet))
.call(simpleCompet -> permService.getSafcaConfig(data.getId())
.call(c -> {
List<Uni<Void>> list = Stream.concat(
Stream.concat(
c.admin().stream().filter(uuid -> !simpleCompet.admin().contains(uuid)),
simpleCompet.admin().stream().filter(uuid -> !c.admin().contains(uuid))),
Stream.concat(
c.table().stream().filter(uuid -> !simpleCompet.table().contains(uuid)),
simpleCompet.table().stream().filter(uuid -> !c.table().contains(uuid)))
).map(uuid -> cacheAccess.invalidate(uuid.toString())).toList();
if (list.isEmpty())
return Uni.createFrom().nullItem();
return Uni.join().all(list).andCollectFailures();
}))
.call(__ -> cache.invalidate(data.getId()));
}
public Uni<List<PresetData>> getPresetsForCompetition(SecurityCtx securityCtx, Long id) {
return permService.hasViewPerm(securityCtx, id)
.chain(cm -> Mutiny.fetch(cm.getCatPreset()))
.map(p -> p.stream().map(PresetData::fromModel).toList());
}
public Uni<Response> unregisterHelloAsso(NotificationData data) {
if (!data.getState().equals("Refunded"))
return Uni.createFrom().item(Response.ok().build());
return helloAssoRepository.list("orderId = ?1", data.getOrder().getId())
.chain(regs -> {
Uni<?> uni = Uni.createFrom().nullItem();
for (HelloAssoRegisterModel reg : regs) {
if (reg.getCompetition().getRegisterMode() != RegisterMode.HELLOASSO)
continue;
if (!data.getOrder().getOrganizationSlug().equalsIgnoreCase(reg.getCompetition().getData1()))
continue;
uni = uni.call(__ -> Panache.withTransaction(
() -> registerRepository.delete("competition = ?1 AND membre = ?2",
reg.getCompetition(), reg.getMembre())))
.call(r -> reg.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterRemove(reg.getCompetition().getUuid(),
reg.getMembre().getId()) : Uni.createFrom().voidItem()).onFailure()
.recoverWithNull()
;
}
return uni;
})
.onFailure().invoke(Throwable::printStackTrace)
.map(__ -> Response.ok().build());
}
public Uni<Response> registerHelloAsso(NotificationData data) {
String organizationSlug = data.getOrganizationSlug();
String formSlug = data.getFormSlug();
RegisterRequestData req = new RegisterRequestData(null, "", "", null, null, 0, false, new ArrayList<>(), null,
Categorie.CADET, Genre.NA, null, "fr", false);
return repository.find("data1 = ?1 AND data2 = ?2", organizationSlug, formSlug).firstResult()
.onFailure().recoverWithNull()
.chain(cm -> {
Uni<?> uni = Uni.createFrom().nullItem();
if (cm == null || cm.getRegisterMode() != RegisterMode.HELLOASSO)
return uni;
List<String> place = List.of(cm.getData3().toLowerCase().split(";"));
List<String> fail = new ArrayList<>();
for (NotificationData.Item item : data.getItems()) {
if (!place.contains(item.getName().toLowerCase()))
continue;
if (item.getCustomFields() == null || item.getCustomFields().isEmpty()) {
fail.add("%s %s - licence n°???".formatted(item.getUser().getLastName(),
item.getUser().getFirstName()));
continue;
}
Optional<Long> optional = item.getCustomFields().stream()
.filter(cf -> cf.getName().equalsIgnoreCase("Numéro de licence")).findAny().map(
NotificationData.CustomField::getAnswer).map(Long::valueOf);
if (optional.isPresent()) {
uni = uni.call(__ -> membreService.getByLicence(optional.get())
.invoke(Unchecked.consumer(m -> {
if (m == null)
throw new NotFoundException();
}))
.call(m -> Panache.withTransaction(() ->
helloAssoRepository.persist(
new HelloAssoRegisterModel(cm, m, data.getId()))))
.chain(m -> updateRegister(req, cm, m, true, true)))
.onFailure().recoverWithItem(throwable -> {
fail.add("%s %s - licence n°%d".formatted(item.getUser().getLastName(),
item.getUser().getFirstName(), optional.get()));
return null;
})
.replaceWithVoid();
} else {
fail.add("%s %s - licence n°???".formatted(item.getUser().getLastName(),
item.getUser().getFirstName()));
}
}
return uni.call(__ -> fail.isEmpty() ? Uni.createFrom().nullItem() :
reactiveMailer.send(
Mail.withText(cm.getData4(),
"FFSAF - Compétition - Erreur HelloAsso",
String.format(
"""
Bonjour,
Une erreur a été rencontrée lors de l'enregistrement d'une inscription à votre compétition %s pour les combattants suivants:
%s
Cordialement,
L'intranet de la FFSAF
""", cm.getName(), String.join("\r\n", fail))
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
})
.onFailure().invoke(Throwable::printStackTrace)
.map(__ -> Response.ok().build());
}
}

View File

@@ -0,0 +1,71 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.rest.client.HelloAssoAuthClient;
import fr.titionfire.ffsaf.rest.client.dto.TokenResponse;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
@ApplicationScoped
public class HelloAssoTokenService {
private static final Logger LOG = Logger.getLogger(HelloAssoTokenService.class);
@RestClient
HelloAssoAuthClient authClient;
@ConfigProperty(name = "helloasso.client-id")
String clientId;
@ConfigProperty(name = "helloasso.client-secret")
String clientSecret;
private TokenResponse currentToken; // Stockage en mémoire (pour un seul pod)
// Récupère un token valide (en le rafraîchissant si nécessaire)
public Uni<String> getValidAccessToken() {
if (clientId == null || clientSecret == null || clientId.isEmpty() || clientSecret.isEmpty() || clientId.equalsIgnoreCase(
"null") || clientSecret.equalsIgnoreCase("null")) {
LOG.error("Client ID ou Client Secret non configuré pour HelloAsso");
return Uni.createFrom().failure(new IllegalStateException("HelloAsso client credentials not configured"));
}
if (currentToken == null || currentToken.isExpired()) {
return fetchNewToken(clientId, clientSecret);
}
return Uni.createFrom().item(currentToken.accessToken);
}
// Récupère un nouveau token (via client_credentials ou refresh_token)
private Uni<String> fetchNewToken(String clientId, String clientSecret) {
if (currentToken != null && currentToken.refreshToken != null) {
// On utilise le refresh_token si disponible
return authClient.refreshToken("refresh_token", clientId, currentToken.refreshToken)
.onItem().invoke(token -> {
LOG.info("Token rafraîchi avec succès");
currentToken = token;
})
.onFailure().recoverWithItem(e -> {
LOG.warn("Échec du rafraîchissement, utilisation des credentials", e);
return null; // Force l'utilisation des credentials
})
.flatMap(token -> token != null ?
Uni.createFrom().item(token.accessToken) :
getTokenWithCredentials(clientId, clientSecret)
);
} else {
return getTokenWithCredentials(clientId, clientSecret);
}
}
// Récupère un token avec client_id/client_secret
private Uni<String> getTokenWithCredentials(String clientId, String clientSecret) {
return authClient.getToken("client_credentials", clientId, clientSecret)
.onItem().invoke(token -> {
LOG.info("Nouveau token obtenu");
currentToken = token;
})
.onFailure().invoke(e -> LOG.error("Erreur lors de l'obtention du token", e))
.map(token -> token.accessToken);
}
}

View File

@@ -2,8 +2,10 @@ package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.KeycloakException;
import fr.titionfire.ffsaf.utils.RequiredAction;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.reactive.ReactiveMailer;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
@@ -21,8 +23,10 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ApplicationScoped
public class KeycloakService {
@@ -40,6 +44,12 @@ public class KeycloakService {
@ConfigProperty(name = "keycloak.realm")
String realm;
@ConfigProperty(name = "email.enabled")
boolean enabled_email;
@Inject
ReactiveMailer reactiveMailer;
@Inject
Vertx vertx;
@@ -47,58 +57,167 @@ public class KeycloakService {
if (club.getClubId() == null) {
LOGGER.infof("Creation of club group %d-%s...", club.getId(), club.getName());
return vertx.getOrCreateContext().executeBlocking(() -> {
GroupRepresentation clubGroup = keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
.findAny().orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));
GroupRepresentation clubGroup =
keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
.findAny()
.orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));
GroupRepresentation groupRepresentation = new GroupRepresentation();
groupRepresentation.setName(club.getId() + "-" + club.getName());
try (Response response = keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) {
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT))
throw new KeycloakException("Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(),
try (Response response =
keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) {
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo()
.equals(Response.Status.CONFLICT))
throw new KeycloakException(
"Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(),
response.getStatusInfo().getReasonPhrase()));
}
return keycloak.realm(realm).groups().group(clubGroup.getId()).getSubGroups(0, 1000, true).stream()
.filter(g -> g.getName().startsWith(club.getId() + "-")).findAny().map(GroupRepresentation::getId)
.orElseThrow(() -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-")));
.filter(g -> g.getName().startsWith(club.getId() + "-")).findAny()
.map(GroupRepresentation::getId)
.orElseThrow(
() -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-")));
}
).call(id -> clubService.setClubId(club.getId(), id));
}
return Uni.createFrom().item(club::getClubId);
}
public Uni<String> updateGroupFromClub(ClubModel club) {
if (club.getClubId() == null) {
return getGroupFromClub(club);
} else {
LOGGER.infof("Updating name of club group %d-%s...", club.getId(), club.getName());
return vertx.getOrCreateContext().executeBlocking(() -> {
GroupRepresentation clubGroup =
keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
.findAny()
.orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));
keycloak.realm(realm).groups().group(clubGroup.getId()).getSubGroups(0, 1000, true).stream()
.filter(g -> g.getName().startsWith(club.getId() + "-")).findAny()
.ifPresent(groupRepresentation -> {
groupRepresentation.setName(club.getId() + "-" + club.getName());
keycloak.realm(realm).groups().group(groupRepresentation.getId())
.update(groupRepresentation);
});
return club.getClubId();
}
);
}
}
public Uni<String> getUserFromMember(MembreModel membreModel) {
if (membreModel.getUserId() == null) {
return Uni.createFrom().failure(new NullPointerException("No keycloak user linked to the user id=" + membreModel.getId()));
return Uni.createFrom()
.failure(new DInternalError("No keycloak user linked to the user id=" + membreModel.getId()));
}
return Uni.createFrom().item(membreModel::getUserId);
}
public Uni<String> setClubGroupMembre(MembreModel membreModel, ClubModel club) {
return getGroupFromClub(club).chain(
clubId -> getUserFromMember(membreModel).chain(userId -> vertx.getOrCreateContext().executeBlocking(() -> {
if (club == null)
return getUserFromMember(membreModel).chain(
userId -> vertx.getOrCreateContext().executeBlocking(() -> {
UserResource user = keycloak.realm(realm).users().get(userId);
user.groups().stream().filter(g -> g.getPath().startsWith("/club")).forEach(g -> user.leaveGroup(g.getId()));
user.groups().stream().filter(g -> g.getPath().startsWith("/club"))
.forEach(g -> user.leaveGroup(g.getId()));
return "OK";
}));
else
return getGroupFromClub(club).chain(
clubId -> getUserFromMember(membreModel).chain(
userId -> vertx.getOrCreateContext().executeBlocking(() -> {
UserResource user = keycloak.realm(realm).users().get(userId);
user.groups().stream().filter(g -> g.getPath().startsWith("/club"))
.forEach(g -> user.leaveGroup(g.getId()));
user.joinGroup(clubId);
LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId, user.toRepresentation().getUsername());
LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId,
user.toRepresentation().getUsername());
return "OK";
})));
}
public Uni<UserCompteState> fetchCompte(String id) {
public Uni<?> setEmail(String userId, String email) {
return vertx.getOrCreateContext().executeBlocking(() -> {
UserResource user = keycloak.realm(realm).users().get(userId);
UserRepresentation user2 = user.toRepresentation();
String oldEmail = user2.getEmail();
if (email.equals(user2.getEmail()))
return null;
user2.setEmail(email);
user2.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name()));
user.update(user2);
if (enabled_email)
user.sendVerifyEmail();
return oldEmail;
}).call(oldEmail -> oldEmail == null || !enabled_email ? Uni.createFrom().item("") :
reactiveMailer.send(
Mail.withHtml(oldEmail, "FFSAF - Changement de votre adresse email",
String.format(Utils.HTML_HEADER, "FFSAF - Changement de votre adresse email") +
String.format("""
<p>Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,<br/>
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.</p>
<p>Ancienne adresse email : <span class="highlight">%s</span><br/>Nouvelle adresse email : <span class="highlight">%s</span></p>
<p>Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
""",
oldEmail, email) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour,
Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.
Ancienne adresse email : %s
Nouvelle adresse email : %s
Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr.
Cordialement,
L'équipe de la FFSAF
""", oldEmail, email))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
}
public Uni<?> setAutoRoleMembre(String id, RoleAsso role, GradeArbitrage gradeArbitrage) {
List<String> toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire",
"club_respo_intra", "asseseur", "arbitre"));
List<String> toAdd = new ArrayList<>();
switch (role) {
case PRESIDENT, VPRESIDENT -> toAdd.add("club_president");
case TRESORIER, VTRESORIER -> toAdd.add("club_tresorier");
case SECRETAIRE, VSECRETAIRE -> toAdd.add("club_secretaire");
case MEMBREBUREAU -> toAdd.add("club_respo_intra");
}
switch (gradeArbitrage) {
case ARBITRE -> toAdd.addAll(List.of("asseseur", "arbitre"));
case ASSESSEUR -> toAdd.add("asseseur");
}
toRemove.removeAll(toAdd);
return updateRole(id, toAdd, toRemove);
}
public Uni<Pair<UserResource, UserCompteState>> fetchCompte(String id) {
return vertx.getOrCreateContext().executeBlocking(() -> {
UserResource user = keycloak.realm(realm).users().get(id);
UserRepresentation user2 = user.toRepresentation();
return new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified(),
user.roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList(),
user.groups().stream().map(GroupRepresentation::getName).toList());
return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(),
user2.isEmailVerified()));
});
}
public Uni<List<String>> fetchRole(String id) {
return vertx.getOrCreateContext().executeBlocking(() ->
keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList());
keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream()
.map(RoleRepresentation::getName).toList());
}
public Uni<?> updateRole(String id, List<String> toAdd, List<String> toRemove) {
@@ -121,15 +240,23 @@ public class KeycloakService {
throw new KeycloakException("User name is null");
})).chain(membreModel -> creatUser(membreModel).chain(user -> {
LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId());
return membreService.setUserId(membreModel.getId(), user.getId());
}))
.map(__ -> "OK");
return membreService.setUserId(membreModel.getId(), user.getId()).map(__ -> user.getId());
}));
}
private Uni<UserRepresentation> creatUser(MembreModel membreModel) {
String login = makeLogin(membreModel);
LOGGER.infof("Creation of user %s...", login);
return vertx.getOrCreateContext().executeBlocking(() -> {
String login;
int i = 1;
do {
login = makeLogin(membreModel);
if (i > 1) {
login += i;
}
i++;
} while (!keycloak.realm(realm).users().searchByUsername(login, true).isEmpty());
LOGGER.infof("Creation of user %s...", login);
UserRepresentation user = new UserRepresentation();
user.setUsername(login);
user.setFirstName(membreModel.getFname());
@@ -137,22 +264,109 @@ public class KeycloakService {
user.setEmail(membreModel.getEmail());
user.setEnabled(true);
user.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name(),
RequiredAction.UPDATE_PASSWORD.name()));
try (Response response = keycloak.realm(realm).users().create(user)) {
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT))
throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login, response.getStatusInfo().getReasonPhrase()));
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo()
.equals(Response.Status.CONFLICT))
throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login,
response.getStatusInfo().getReasonPhrase()));
}
return getUser(login).orElseThrow(() -> new KeycloakException("Fail to fetch user %s".formatted(login)));
String finalLogin = login;
return getUser(login).orElseThrow(
() -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin)));
})
.invoke(user -> membreModel.setUserId(user.getId()))
.call(user -> updateRole(user.getId(), List.of("safca_user"), List.of()))
.call(user -> enabled_email ? reactiveMailer.send(
Mail.withHtml(user.getEmail(), "FFSAF - Creation de votre compte sur l'intranet",
String.format(Utils.HTML_HEADER, "Création de votre compte intranet FFSAF") +
String.format(
"""
<p>Suite &agrave; votre premi&egrave;re inscription <span class="highlight">%s</span> &agrave; la <strong>F&eacute;d&eacute;ration France Soft Armored Fighting (FFSAF)</strong>, votre compte intranet a &eacute;t&eacute; cr&eacute;&eacute;.</p>
<p>Ce compte vous permettra de :</p>
<ul>
<li>Consulter vos informations personnelles,</li>
<li>Vous inscrire aux comp&eacute;titions (bient&ocirc;t disponible),</li>
<li>Consulter les r&eacute;sultats des comp&eacute;titions.</li>
</ul>
<p>L&rsquo;intranet est accessible &agrave; l&rsquo;adresse suivante :</p>
<p style="text-align: center;"><a href="https://intra.ffsaf.fr" class="button">Acc&eacute;der &agrave; l&apos;intranet</a></p>
<p>Votre nom d&rsquo;utilisateur est : <span class="highlight">%s</span></p>
<p>Pour d&eacute;finir votre mot de passe, rendez-vous sur l&rsquo;intranet : <strong>Connexion &gt; Mot de passe oubli&eacute; ?</strong></p>
<p>Si vous n&rsquo;avez pas demand&eacute; cette inscription, veuillez contacter le support &agrave; l&rsquo;adresse : <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
<p class="footer">(Pas de panique, nous ne vous enverrons pas de message autre que ceux concernant votre compte.)</p>
""",
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
.getName() + ") " : "", user.getUsername()) +
Utils.HTML_FOOTER
)
.setText(String.format(
"""
Bonjour,
Suite à votre première inscription %sà la Fédération France Soft Armored Fighting (FFSAF), votre compte intranet a été créé.
Ce compte vous permettra de consulter vos informations et, dans un futur proche, de vous inscrire aux compétitions ainsi que d'en consulter les résultats.
L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr
Votre nom d'utilisateur est : %s
Pour définir votre mot de passe, rendez-vous sur l'intranet > "Connexion" > "Mot de passe oublié ?"
Si vous n'avez pas demandé cette inscription, veuillez contacter le support à l'adresse support@ffsaf.fr.
(Pas de panique, nous ne vous enverrons pas de message autre que ce concernant votre compte)
Cordialement,
L'équipe de la FFSAF
""",
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
.getName() + ") " : "", user.getUsername()))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
) : Uni.createFrom().nullItem())
.call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
.call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));
}
private Optional<UserRepresentation> getUser(String username) {
public Uni<?> setId(long id, String nid) {
return membreService.setUserId(id, nid).map(__ -> "OK");
}
public Uni<?> removeAccount(String userId) {
return vertx.getOrCreateContext().executeBlocking(() -> {
try (Response response = keycloak.realm(realm).users().delete(userId)) {
System.out.println(response.getStatusInfo());
if (!response.getStatusInfo().equals(Response.Status.NO_CONTENT))
throw new KeycloakException("Fail to delete user %s (reason=%s)".formatted(userId,
response.getStatusInfo().getReasonPhrase()));
}
return null;
});
}
public Uni<?> removeClubGroup(String clubId) {
return vertx.getOrCreateContext().executeBlocking(() -> {
keycloak.realm(realm).groups().group(clubId).remove();
return null;
});
}
public Uni<?> clearUser(String userId) {
List<String> toRemove = new ArrayList<>(
List.of("club_president", "club_tresorier", "club_secretaire", "club_respo_intra"));
return vertx.getOrCreateContext().executeBlocking(() -> {
UserResource user = keycloak.realm(realm).users().get(userId);
RoleScopeResource resource = user.roles().realmLevel();
List<RoleRepresentation> roles = keycloak.realm(realm).roles().list();
resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList());
user.groups().stream().filter(g -> g.getPath().startsWith("/club"))
.forEach(g -> user.leaveGroup(g.getId()));
return "OK";
});
}
public Optional<UserRepresentation> getUser(String username) {
List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);
if (users.isEmpty())
@@ -161,14 +375,32 @@ public class KeycloakService {
return Optional.of(users.get(0));
}
public Optional<UserRepresentation> getUser(UUID userId) {
return getUserById(userId.toString());
}
public Optional<UserRepresentation> getUserById(String userId) {
try {
UserResource user = keycloak.realm(realm).users().get(userId);
if (user == null)
return Optional.empty();
else
return Optional.of(user.toRepresentation());
} catch (Exception e) {
return Optional.empty();
}
}
private String makeLogin(MembreModel model) {
return Normalizer.normalize((model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'), Normalizer.Form.NFD)
return Normalizer.normalize(
(model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'),
Normalizer.Form.NFD)
.replaceAll("\\p{M}", "");
}
@RegisterForReflection
public record UserCompteState(Boolean enabled, String login, Boolean emailVerified, List<String> realmRoles,
List<String> groups) {
public record UserCompteState(Boolean enabled, String login, Boolean emailVerified) {
}
}

View File

@@ -0,0 +1,258 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.LicenceModel;
import fr.titionfire.ffsaf.data.model.LogModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
import fr.titionfire.ffsaf.data.repository.SequenceRepository;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.from.LicenceForm;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.utils.SequenceType;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
@WithSession
@ApplicationScoped
public class LicenceService {
private static final Logger LOGGER = Logger.getLogger(LicenceService.class);
@Inject
LicenceRepository repository;
@Inject
CombRepository combRepository;
@Inject
SequenceRepository sequenceRepository;
@Inject
KeycloakService keycloakService;
@Inject
LoggerService ls;
@Inject
CheckoutService checkoutService;
@Inject
TradService trad;
public Uni<List<LicenceModel>> getLicence(long id, Consumer<MembreModel> checkPerm) {
return combRepository.findById(id).invoke(checkPerm)
.chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
}
public Uni<List<LicenceModel>> getCurrentSaisonLicence(SecurityCtx securityCtx) {
if (securityCtx == null || securityCtx.getSubject() == null)
return repository.find("saison = ?1", Utils.getSaison()).list();
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult().map(MembreModel::getClub)
.chain(clubModel -> combRepository.find("club = ?1", clubModel).list())
.chain(membres -> repository.find("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres).list());
}
public Uni<?> valideLicences(List<Long> ids) {
Uni<String> uni = Uni.createFrom().nullItem();
for (Long id : ids) {
uni = uni.chain(__ -> repository.find("membre.id = ?1 AND saison = ?2", id, Utils.getSaison()).firstResult()
.chain(model -> {
if (!model.isValidate())
ls.logUpdate("validation de la licence", model);
return validateLicences(model);
}))
.map(__ -> "OK");
}
return uni.call(__ -> ls.append());
}
protected Uni<LicenceModel> validateLicences(LicenceModel model) {
model.setValidate(true);
return Panache.withTransaction(() -> repository.persist(model)
.call(m -> Mutiny.fetch(m.getMembre())
.call(genLicenceNumberAndAccountIfNeed())
));
}
public Uni<?> validePaymentLicences(List<Long> ids) {
Uni<String> uni = Uni.createFrom().nullItem();
for (Long id : ids) {
uni = uni.chain(__ -> repository.find("membre.id = ?1 AND saison = ?2", id, Utils.getSaison()).firstResult()
.chain(model -> {
if (!model.isPay())
ls.logUpdate("payment (admin) de la licence", model);
return validatePayLicences(model);
}))
.map(__ -> "OK");
}
return uni.call(__ -> ls.append());
}
protected Uni<LicenceModel> validatePayLicences(LicenceModel model) {
model.setPay(true);
return Panache.withTransaction(() -> repository.persist(model));
}
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
if (form.getId() == -1) {
return combRepository.findById(id).chain(membreModel -> {
LicenceModel model = new LicenceModel();
model.setMembre(membreModel);
model.setClub_id((membreModel.getClub() == null) ? null : membreModel.getClub().getId());
model.setSaison(form.getSaison());
model.setCertificate(form.getCertificate());
model.setValidate(form.isValidate());
model.setPay(form.isPay());
return Panache.withTransaction(() -> repository.persist(model)
.call(m -> m.isValidate() ? Uni.createFrom().item(membreModel)
.call(genLicenceNumberAndAccountIfNeed())
: Uni.createFrom().nullItem()
))
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(),
licenceModel));
});
} else {
return repository.findById(form.getId()).chain(model -> {
ls.logChange("Certificate", model.getCertificate(), form.getCertificate(), model);
ls.logChange("Validate", model.isValidate(), form.isValidate(), model);
ls.logChange("Pay", model.isPay(), form.isPay(), model);
model.setCertificate(form.getCertificate());
model.setValidate(form.isValidate());
model.setPay(form.isPay());
return Panache.withTransaction(() -> repository.persist(model)
.call(m -> m.isValidate() ? Mutiny.fetch(m.getMembre())
.call(genLicenceNumberAndAccountIfNeed())
: Uni.createFrom().nullItem()
))
.call(__ -> ls.append());
});
}
}
private Function<MembreModel, Uni<?>> genLicenceNumberAndAccountIfNeed() {
return membreModel -> ((membreModel.getLicence() == null || membreModel.getLicence() <= 0) ?
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
.chain(() -> combRepository.persist(membreModel))
: Uni.createFrom().nullItem())
.call(__ -> (membreModel.getUserId() == null) ?
keycloakService.initCompte(membreModel.getId()).onFailure()
.invoke(t -> LOGGER.infof("Failed to init account: %s", t.getMessage())).onFailure()
.recoverWithNull()
: Uni.createFrom().nullItem());
}
public Uni<String> payLicences(List<Long> ids, Consumer<MembreModel> checkPerm, SecurityCtx securityCtx) {
return repository.list("membre.id IN ?1 AND saison = ?2 AND pay = FALSE", ids, Utils.getSaison())
.invoke(Unchecked.consumer(models -> {
if (models.size() != ids.size())
throw new DBadRequestException(trad.t("erreur.lors.de.la.selection.des.membres"));
}))
.call(models -> {
Uni<?> uni = Uni.createFrom().nullItem();
for (LicenceModel model : models)
uni = uni.chain(__ -> Mutiny.fetch(model.getMembre()).invoke(checkPerm));
return uni;
})
.chain(models -> checkoutService.create(models.stream().map(LicenceModel::getId).toList(),
securityCtx));
}
public Uni<?> deleteLicence(long id) {
return repository.findById(id)
.call(__ -> checkoutService.canDeleteLicence(id)
.invoke(Unchecked.consumer(b -> {
if (!b) throw new DBadRequestException(
trad.t("licence.rm.err1"));
})))
.call(model -> ls.logADelete(model))
.chain(model -> Panache.withTransaction(() -> repository.delete(model)));
}
public Uni<LicenceModel> askLicence(long id, LicenceForm form, Consumer<MembreModel> checkPerm) {
return combRepository.findById(id).invoke(checkPerm).chain(membreModel -> {
if (form.getId() == -1) {
return repository.find("saison = ?1 AND membre = ?2", Utils.getSaison(), membreModel).count()
.invoke(Unchecked.consumer(count -> {
if (count > 0)
throw new DBadRequestException(trad.t("licence.deja.demandee"));
})).chain(__ -> combRepository.findById(id).chain(membreModel2 -> {
LicenceModel model = new LicenceModel();
model.setClub_id((membreModel2.getClub() == null) ? null : membreModel2.getClub().getId());
model.setMembre(membreModel2);
model.setSaison(Utils.getSaison());
model.setCertificate(form.getCertificate());
model.setValidate(false);
return Panache.withTransaction(() -> repository.persist(model));
}))
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(),
licenceModel));
} else {
return repository.findById(form.getId()).chain(model -> {
ls.logChange("Certificate", model.getCertificate(), form.getCertificate(), model);
model.setCertificate(form.getCertificate());
return Panache.withTransaction(() -> repository.persist(model))
.call(__ -> ls.append());
});
}
});
}
public Uni<?> deleteAskLicence(long id, Consumer<MembreModel> checkPerm) {
return repository.findById(id)
.call(licenceModel -> Mutiny.fetch(licenceModel.getMembre()).invoke(checkPerm))
.call(__ -> checkoutService.canDeleteLicence(id)
.invoke(Unchecked.consumer(b -> {
if (!b) throw new DBadRequestException(
trad.t("licence.rm.err1"));
})))
.invoke(Unchecked.consumer(licenceModel -> {
if (licenceModel.isValidate())
throw new DBadRequestException(trad.t("impossible.de.supprimer.une.licence.deja.validee"));
if (licenceModel.isPay())
throw new DBadRequestException(trad.t("impossible.de.supprimer.une.licence.deja.payee"));
}))
.call(model -> ls.logADelete(model))
.chain(__ -> Panache.withTransaction(() -> repository.deleteById(id)));
}
public Uni<?> setImport(int licence, int saison, boolean valid) {
return combRepository.find("licence = ?1", licence).firstResult()
.chain(membreModel ->
repository.find("saison = ?1 AND membre = ?2", saison, membreModel).firstResult()
.chain(licenceModel -> {
if (licenceModel != null) {
if (licenceModel.getClub_id() == null)
licenceModel.setClub_id(
(membreModel.getClub() == null) ? null : membreModel.getClub()
.getId());
licenceModel.setValidate(valid);
return Panache.withTransaction(() -> repository.persist(licenceModel));
} else {
LicenceModel model = new LicenceModel();
model.setClub_id(
(membreModel.getClub() == null) ? null : membreModel.getClub().getId());
model.setMembre(membreModel);
model.setSaison(saison);
model.setCertificate("¤");
model.setValidate(valid);
return Panache.withTransaction(() -> repository.persist(model));
}
}))
.map(__ -> "OK");
}
}

View File

@@ -0,0 +1,111 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.LogModel;
import fr.titionfire.ffsaf.data.model.LogModel.ActionType;
import fr.titionfire.ffsaf.data.model.LogModel.ObjectType;
import fr.titionfire.ffsaf.data.model.LoggableModel;
import fr.titionfire.ffsaf.data.repository.LogRepository;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@WithSession
@RequestScoped
public class LoggerService {
@Inject
LogRepository repository;
@Inject
SecurityCtx securityCtx;
private final List<LogModel> buffer = new ArrayList<>();
public Uni<?> logA(ActionType action, ObjectType object, String message, String target_name, Long target_id) {
return Panache.withTransaction(() -> repository.persist(
new LogModel(null, securityCtx.getSubject(), new Date(), action, object, target_id, target_name,
message)));
}
public Uni<?> logA(ActionType action, String message, LoggableModel model) {
return logA(action, model.getObjectType(), message, model.getObjectName(), model.getId());
}
public Uni<?> logAAdd(LoggableModel model) {
return logA(ActionType.ADD, "", model);
}
public Uni<?> logAUpdate(String message, LoggableModel model) {
return logA(ActionType.UPDATE, message, model);
}
public Uni<?> logAChange(String champ, Object o1, Object o2, LoggableModel model) {
if (Objects.equals(o1, o2))
return Uni.createFrom().nullItem();
return logA(ActionType.UPDATE, champ + ": " + o1.toString() + " -> " + o2.toString(), model);
}
public Uni<?> logADelete(LoggableModel model) {
return logA(ActionType.REMOVE, "", model);
}
public Uni<?> append() {
if (buffer.isEmpty())
return Uni.createFrom().voidItem();
return Panache.withTransaction(() -> repository.persist(buffer))
.invoke(__ -> buffer.clear());
}
public void clear() {
buffer.clear();
}
public void log(ActionType action, ObjectType object, String message, String target_name, Long target_id) {
buffer.add(new LogModel(null, securityCtx.getSubject(), new Date(), action, object, target_id, target_name,
message));
}
public void logAnonymous(ActionType action, ObjectType object, String message, String target_name, Long target_id) {
buffer.add(new LogModel(null, null, new Date(), action, object, target_id, target_name, message));
}
public void log(ActionType action, String message, LoggableModel model) {
log(action, model.getObjectType(), message, model.getObjectName(), model.getId());
}
public void logAnonymous(ActionType action, String message, LoggableModel model) {
logAnonymous(action, model.getObjectType(), message, model.getObjectName(), model.getId());
}
public void logAdd(LoggableModel model) {
log(ActionType.ADD, "", model);
}
public void logUpdate(String message, LoggableModel model) {
log(ActionType.UPDATE, message, model);
}
public void logUpdateAnonymous(String message, LoggableModel model) {
logAnonymous(ActionType.UPDATE, message, model);
}
public void logChange(String champ, Object o1, Object o2, LoggableModel model) {
if (Objects.equals(o1, o2))
return;
log(ActionType.UPDATE,
champ + ": " + (o1 == null ? "null" : o1.toString()) + " -> " + (o2 == null ? "null" : o2.toString()),
model);
}
public void logDelete(LoggableModel model) {
log(ActionType.REMOVE, "", model);
}
}

View File

@@ -0,0 +1,132 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.repository.CategoryRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.rest.data.MatchData;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
@WithSession
@ApplicationScoped
public class MatchService {
@Inject
MatchRepository repository;
@Inject
CategoryRepository categoryRepository;
@Inject
CombRepository combRepository;
@Inject
CompetPermService permService;
@Inject
CompetitionGuestRepository competitionGuestRepository;
public Uni<MatchData> getByIdAdmin(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult()
.onItem().ifNull().failWith(() -> new DNotFoundException("Match not found"))
.call(data -> permService.hasAdminViewPerm(securityCtx, data.getCategory().getCompet()))
.map(MatchData::fromModel);
}
public Uni<List<MatchData>> getAllByPouleAdmin(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
return categoryRepository.find("systemId = ?1 AND system = ?2", id, system).firstResult()
.onItem().ifNull().failWith(() -> new DNotFoundException("Poule not found"))
.call(data -> permService.hasAdminViewPerm(securityCtx, data.getCompet()))
.chain(data -> repository.list("poule = ?1", data.getId())
.map(o -> o.stream().map(MatchData::fromModel).toList()));
}
public Uni<MatchData> addOrUpdate(SecurityCtx securityCtx, CompetitionSystem system, MatchData data) {
return repository.find("systemId = ?1 AND system = ?2", data.getId(), system).firstResult()
.chain(o -> {
if (o == null) {
return categoryRepository.find("systemId = ?1 AND system = ?2", data.getCategory(), system)
.firstResult()
.onItem().ifNull().failWith(() -> new DNotFoundException("Poule not found"))
.call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet()))
.map(categoryModel -> {
MatchModel model = new MatchModel();
model.setId(null);
model.setSystem(system);
model.setSystemId(data.getId());
model.setCategory(categoryModel);
return model;
});
} else {
return categoryRepository.find("systemId = ?1 AND system = ?2", data.getCategory(), system)
.firstResult()
.onItem().ifNull().failWith(() -> new DNotFoundException("Poule not found"))
.call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet()))
.map(__ -> o);
}
}
)
.chain(o -> {
o.setCategory_ord(data.getCategory_ord());
o.getScores().clear();
o.getScores().addAll(data.getScores());
return Uni.createFrom().nullItem()
.chain(() -> (data.getC1_id() == null) ?
Uni.createFrom().nullItem() : combRepository.findById(data.getC1_id()))
.invoke(o::setC1_id)
.chain(() -> (data.getC1_id() == null) ?
Uni.createFrom().nullItem() : combRepository.findById(data.getC2_id()))
.invoke(o::setC2_id)
.chain(() -> (data.getC1_str() == null) ?
Uni.createFrom()
.item((CompetitionGuestModel) null) : competitionGuestRepository.find(
"fname = ?1 AND lname = ?2",
data.getC1_str().substring(0, data.getC1_str().indexOf(" ")),
data.getC1_str().substring(data.getC1_str().indexOf(" ") + 1)).firstResult())
.invoke(o::setC1_guest)
.chain(() -> (data.getC2_str() == null) ?
Uni.createFrom()
.item((CompetitionGuestModel) null) : competitionGuestRepository.find(
"fname = ?1 AND lname = ?2",
data.getC2_str().substring(0, data.getC2_str().indexOf(" ")),
data.getC2_str().substring(data.getC2_str().indexOf(" ") + 1)).firstResult())
.invoke(o::setC2_guest)
.chain(() -> Panache.withTransaction(() -> repository.persist(o)));
})
.map(MatchData::fromModel);
}
public Uni<?> updateScore(SecurityCtx securityCtx, CompetitionSystem system, Long id,
List<ScoreEmbeddable> scores) {
return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult()
.onItem().ifNull().failWith(() -> new DNotFoundException("Match not found"))
.call(o2 -> permService.hasEditPerm(securityCtx, o2.getCategory().getCompet()))
.invoke(data -> {
data.getScores().clear();
data.getScores().addAll(scores);
})
.chain(data -> Panache.withTransaction(() -> repository.persist(data)))
.map(o -> "OK");
}
public Uni<?> delete(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult()
.onItem().ifNull().failWith(() -> new DNotFoundException("Match not found"))
.call(o2 -> permService.hasEditPerm(securityCtx, o2.getCategory().getCompet()))
.chain(data -> Panache.withTransaction(() -> repository.delete(data)));
}
}

View File

@@ -1,82 +1,627 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.LicenceModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
import fr.titionfire.ffsaf.net2.request.SReqComb;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.utils.Pair;
import fr.titionfire.ffsaf.rest.from.UserSettingForm;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.scheduler.Scheduled;
import io.quarkus.vertx.VertxContextSupport;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@WithSession
@ApplicationScoped
public class MembreService {
private static final Logger LOGGER = Logger.getLogger(MembreService.class);
@Inject
CombRepository repository;
@Inject
ClubRepository clubRepository;
@Inject
LicenceRepository licenceRepository;
@Inject
CompetitionRepository competitionRepository;
@Inject
ServerCustom serverCustom;
@Inject
KeycloakService keycloakService;
@ConfigProperty(name = "upload_dir")
String media;
@Inject
RegisterRepository registerRepository;
@Inject
LoggerService ls;
@Inject
TradService trad;
public SimpleCombModel find(int licence, String np) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
repository.find("licence = ?1 AND (lname collate UTF8_GENERAL_CI LIKE ?2 OR fname collate UTF8_GENERAL_CI LIKE ?2)",
repository.find(
"licence = ?1 AND (unaccent(lname) ILIKE unaccent(?2) OR unaccent(fname) ILIKE unaccent(?2))",
licence, np).firstResult().map(SimpleCombModel::fromModel)));
}
public SimpleCombModel findByIdOptionalComb(long id) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleCombModel::fromModel)));
return VertxContextSupport.subscribeAndAwait(
() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleCombModel::fromModel)));
}
public Uni<List<MembreModel>> getAll() {
return repository.listAll(Sort.ascending("fname", "lname"));
final static String FIND_NAME_REQUEST = "unaccent(fname) ILIKE unaccent(?1) OR unaccent(lname) ILIKE unaccent(?1) " +
"OR unaccent(fname || ' ' || lname) ILIKE unaccent(?1) OR unaccent(lname || ' ' || fname) ILIKE unaccent(?1)";
private Uni<List<LicenceModel>> getLicenceListe(int licenceRequest, int payState) {
Uni<List<LicenceModel>> baseUni;
String queryStr = "saison = ?1";
if (payState == 0)
queryStr += " AND pay = FALSE";
if (payState == 1)
queryStr += " AND pay = TRUE";
if (licenceRequest == 0 || licenceRequest == 1)
baseUni = licenceRepository.list(queryStr, Utils.getSaison());
else if (licenceRequest == 2)
baseUni = licenceRepository.list(queryStr + " AND validate = FALSE", Utils.getSaison());
else if (licenceRequest == 5)
baseUni = licenceRepository.list(queryStr + " AND validate = FALSE AND LENGTH(certificate) >= 3",
Utils.getSaison());
else if (licenceRequest == 6)
baseUni = licenceRepository.list(queryStr + " AND validate = FALSE AND LENGTH(certificate) <= 2",
Utils.getSaison());
else if (licenceRequest == 3)
baseUni = licenceRepository.list(queryStr + " AND validate = TRUE", Utils.getSaison());
else
baseUni = Uni.createFrom().item(new ArrayList<>());
return baseUni;
}
private Sort getSort(String order) {
Sort sort;
if (order == null || order.isBlank()) {
sort = Sort.ascending("fname", "lname");
} else {
sort = Sort.empty();
for (String e : order.split(",")) {
String[] split = e.split(" ");
if (split.length == 2) {
sort = sort.and(split[0],
split[1].equals("n") ? Sort.Direction.Ascending : Sort.Direction.Descending);
} else {
return null;
}
}
}
return sort;
}
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
String order, String categorie, Boolean archive, String subject) {
return repository.find("userId = ?1", subject).firstResult()
.invoke(Unchecked.consumer(membreModel -> {
if (membreModel == null || membreModel.getClub() == null)
throw new DForbiddenException();
}))
.chain(membreModel ->
searchAdmin(limit, page, search, membreModel.getClub(), licenceRequest, payState, order,
categorie, archive));
}
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, Object club,
int licenceRequest, int payState, String order, String categorie,
Boolean archive) {
if (search == null)
search = "";
search = "%" + search.replaceAll(" ", "% %") + "%";
String categorieFilter;
if (categorie == null || categorie.isBlank())
categorieFilter = " True";
else
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
String archiveFilter;
if (archive == null || archive)
archiveFilter = "True";
else
archiveFilter = String.format(
"EXISTS (SELECT 1 FROM LicenceModel l WHERE l.membre.id = m.id AND l.saison >= %s)",
Utils.getSaison() - 1);
String clubFilter = "(TRUE OR ?3 = ?3)";
if (club != null) {
if (club instanceof String club_) {
if (!club_.isBlank()) {
if (club_.equals("null"))
clubFilter = "club IS NULL AND ?3 = ?3";
else
clubFilter = "LOWER(club.name) LIKE LOWER(?3)";
}
} else if (club instanceof ClubModel)
clubFilter = "club = ?3";
}
String queryStr = String.format(
"SELECT m FROM MembreModel m WHERE id %s ?2 AND %s AND (%s) AND %s AND %s",
(licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN", clubFilter, FIND_NAME_REQUEST,
categorieFilter,
archiveFilter);
Sort sort = getSort(order);
if (sort == null)
return Uni.createFrom().failure(new DInternalError(trad.t("erreur.lors.calcul.du.trie")));
String finalSearch = search;
return getLicenceListe(licenceRequest, payState)
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> {
PanacheQuery<MembreModel> query;
query = repository.find(queryStr, sort, finalSearch, ids, club).page(Page.ofSize(limit));
return getPageResult(query, limit, page);
});
}
private Uni<PageResult<SimpleMembre>> getPageResult(PanacheQuery<MembreModel> query, int limit, int page) {
return Uni.createFrom().item(new PageResult<SimpleMembre>())
.invoke(result -> result.setPage(page))
.invoke(result -> result.setPage_size(limit))
.call(result -> query.count().invoke(result::setResult_count))
.call(result -> query.pageCount()
.invoke(Unchecked.consumer(pages -> {
if (page > pages) throw new DBadRequestException(trad.t("page.out.of.range"));
}))
.invoke(result::setPage_count))
.call(result -> query.page(Page.of(page, limit)).list()
.map(membreModels -> membreModels.stream().map(SimpleMembre::fromModel).toList())
.invoke(result::setResult));
}
public Uni<List<SimpleMembreInOutData>> getAllExport(String subject) {
return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> repository.list("club = ?1", membreModel.getClub()))
.chain(membres -> licenceRepository.list("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres)
.map(l -> membres.stream().map(m -> SimpleMembreInOutData.fromModel(m, l)).toList()));
}
public Uni<String> allImporte(String subject, List<SimpleMembreInOutData> data) {
if (data == null)
return Uni.createFrom().nullItem();
final List<SimpleMembreInOutData> data2 = data.stream()
.filter(dataIn -> dataIn.getNom() != null && !dataIn.getNom()
.isBlank() && dataIn.getPrenom() != null && !dataIn.getPrenom().isBlank()).toList();
if (data2.isEmpty())
return Uni.createFrom().nullItem();
AtomicReference<ClubModel> clubModel = new AtomicReference<>();
LOGGER.debugf("Membre import (size=%d)", data2.size());
for (SimpleMembreInOutData simpleMembreInOutData : data2) {
LOGGER.debugf("-> %s", simpleMembreInOutData.toString());
}
return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> {
clubModel.set(membreModel.getClub());
if (data2.stream().noneMatch(d -> d.getLicence() != null))
return Uni.createFrom().item(new ArrayList<MembreModel>());
return repository.list("licence IN ?1 OR LOWER(lname || ' ' || fname) IN ?2 OR email IN ?3",
data2.stream().map(SimpleMembreInOutData::getLicence).filter(Objects::nonNull).toList(),
data2.stream().map(o -> (o.getNom() + " " + o.getPrenom()).toLowerCase()).toList(),
data2.stream().map(SimpleMembreInOutData::getEmail).filter(o -> o != null && !o.isBlank())
.toList());
})
.call(Unchecked.function(membres -> {
for (MembreModel membreModel : membres) {
if (!Objects.equals(membreModel.getClub(), clubModel.get())) {
LOGGER.info("Similar membres found: " + membreModel);
throw new DForbiddenException(String.format(trad.t("le.membre.appartient.pas.a.votre.club"),
membreModel.getLicence()));
}
}
Uni<Void> uniResult = Uni.createFrom().voidItem();
for (SimpleMembreInOutData dataIn : data2) {
MembreModel model = membres.stream()
.filter(m -> (dataIn.getLicence() != null && Objects.equals(m.getLicence(),
dataIn.getLicence())) || m.getLname().equals(dataIn.getNom()) && m.getFname()
.equals(dataIn.getPrenom()) || (dataIn.getEmail() != null && !dataIn.getEmail()
.isBlank() && Objects.equals(m.getFname(), dataIn.getEmail()))).findFirst()
.orElseGet(() -> {
MembreModel mm = new MembreModel();
mm.setClub(clubModel.get());
mm.setLicences(new ArrayList<>());
mm.setCountry("FR");
return mm;
});
if (model.getId() != null) {
LOGGER.debugf("updating -> %s", dataIn.toString());
} else {
LOGGER.debugf("creating -> %s", dataIn.toString());
}
if (model.getEmail() != null && !model.getEmail().isBlank()) {
if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) {
LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException(
String.format(trad.t("email.deja.utilise.par"), model.getEmail(),
model.getFname(), model.getLname()));
}
if (StringSimilarity.similarity(model.getLname().toUpperCase(),
dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity(
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) {
LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException(
String.format(trad.t("email.deja.utilise.par"), model.getEmail(),
model.getFname(), model.getLname()));
}
}
boolean add = model.getId() == null;
if ((!add && StringSimilarity.similarity(model.getLname().toUpperCase(),
dataIn.getNom().toUpperCase()) > 3) || (!add && StringSimilarity.similarity(
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) {
LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException(
String.format(trad.t("try.edit.licence"), model.getLicence(), model.getFname(),
model.getFname()));
}
ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model);
ls.logChange("Prénom", model.getFname(),
dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1), model);
model.setLname(dataIn.getNom().toUpperCase());
model.setFname(Utils.formatPrenom(dataIn.getPrenom()));
if (dataIn.getEmail() != null && !dataIn.getEmail().isBlank()) {
ls.logChange("Email", model.getEmail(), dataIn.getEmail(), model);
model.setEmail(dataIn.getEmail());
}
model.setGenre(Genre.fromString(dataIn.getGenre()));
if (dataIn.getBirthdate() != null) {
if (model.getBirth_date() == null || !Objects.equals(model.getBirth_date().getTime(),
dataIn.getBirthdate().getTime()))
ls.logChange("Date de naissance", model.getBirth_date(), dataIn.getBirthdate(), model);
model.setBirth_date(dataIn.getBirthdate());
model.setCategorie(Utils.getCategoryFormBirthDate(model.getBirth_date(), new Date()));
}
uniResult = uniResult
.call(() -> Panache.withTransaction(() -> repository.persist(model)
.chain(membreModel1 -> dataIn.isLicenceCurrent() ? licenceRepository.find(
"membre.id = ?1 AND saison = ?2", membreModel1.getId(),
Utils.getSaison())
.firstResult()
.call(l -> {
if (l == null) {
l = new LicenceModel();
l.setMembre(membreModel1);
l.setClub_id(clubModel.get().getId());
l.setValidate(false);
l.setSaison(Utils.getSaison());
}
l.setCertificate(dataIn.getCertif());
return licenceRepository.persist(l);
}) : licenceRepository.delete(
"membre = ?1 AND saison = ?2 AND validate = false", membreModel1,
Utils.getSaison()))));
if (add)
uniResult = uniResult.call(() -> ls.logAAdd(model));
else
uniResult = uniResult.call(() -> ls.append());
}
return uniResult;
}))
.map(__ -> "OK");
}
public Uni<MembreModel> getById(long id) {
return repository.findById(id);
}
public Uni<String> update(long id, FullMemberForm membre) {
public Uni<MembreModel> getByIdWithLicence(long id) {
return repository.findById(id)
.chain(membreModel -> clubRepository.findById(membre.getClub()).map(club -> new Pair<>(membreModel, club)))
.onItem().transformToUni(pair -> {
.call(m -> Mutiny.fetch(m.getLicences()));
}
public Uni<MembreModel> getByAccountId(String subject) {
return repository.find("userId = ?1", subject).firstResult();
}
public Uni<MembreModel> getByLicence(long licence) {
return repository.find("licence = ?1", licence).firstResult();
}
public Uni<String> update(long id, FullMemberForm membre) {
return update(repository.findById(id)
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> {
if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException(trad.t("email.deja.utilise"));
})))
.chain(membreModel -> clubRepository.findById(membre.getClub())
.map(club -> new Pair<>(membreModel, club)))
.onItem().transform(pair -> {
MembreModel m = pair.getKey();
m.setFname(membre.getFname());
m.setLname(membre.getLname());
m.setClub(pair.getValue());
m.setCountry(membre.getCountry());
m.setBirth_date(membre.getBirth_date());
m.setGenre(membre.getGenre());
m.setCategorie(membre.getCategorie());
ls.logChange("Rôle", m.getRole(), membre.getRole(), m);
m.setRole(membre.getRole());
ls.logChange("Club", m.getClub(), pair.getValue(), m);
m.setClub(pair.getValue());
ls.logChange("Grade d'arbitrage", m.getGrade_arbitrage(), membre.getGrade_arbitrage(), m);
m.setGrade_arbitrage(membre.getGrade_arbitrage());
m.setEmail(membre.getEmail());
return Panache.withTransaction(() -> repository.persist(m));
return m;
}), membre, true);
}
public Uni<String> update(long id, FullMemberForm membre, SecurityCtx securityCtx) {
return update(repository.findById(id)
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> {
if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException(trad.t("email.deja.utilise"));
})))
.invoke(Unchecked.consumer(membreModel -> {
if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
throw new DForbiddenException();
if (StringSimilarity.similarity(membreModel.getLname().toUpperCase(),
membre.getLname().toUpperCase()) > 3 || StringSimilarity.similarity(
membreModel.getFname().toUpperCase(), membre.getFname().toUpperCase()) > 3) {
throw new DBadRequestException(
trad.t("regiter.new.membre"));
}
}))
.invoke(Unchecked.consumer(membreModel -> {
RoleAsso source = RoleAsso.MEMBRE;
if (securityCtx.roleHas("club_president")) source = RoleAsso.PRESIDENT;
else if (securityCtx.roleHas("club_secretaire")) source = RoleAsso.SECRETAIRE;
else if (securityCtx.roleHas("club_respo_intra")) source = RoleAsso.MEMBREBUREAU;
if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level >= source.level)
throw new DForbiddenException(trad.t("permission.insuffisante"));
}))
.onItem().transform(target -> {
if (!securityCtx.getSubject().equals(target.getUserId())) {
ls.logChange("Rôle", target.getRole(), membre.getRole(), target);
target.setRole(membre.getRole());
}
return target;
}), membre, false);
}
private Uni<String> update(Uni<MembreModel> uni, FullMemberForm membre, boolean admin) {
return uni.chain(target -> {
ls.logChange("Prénom", target.getFname(), membre.getFname(), target);
target.setFname(Utils.formatPrenom(membre.getFname()));
ls.logChange("Nom", target.getLname(), membre.getLname(), target);
target.setLname(membre.getLname().toUpperCase());
ls.logChange("Pays", target.getCountry(), membre.getCountry(), target);
target.setCountry(membre.getCountry());
if (membre.getBirth_date() != null && (target.getBirth_date() == null || !Objects.equals(
target.getBirth_date().getTime(), membre.getBirth_date().getTime()))) {
ls.logChange("Date de naissance", target.getBirth_date(), membre.getBirth_date(), target);
target.setBirth_date(membre.getBirth_date());
target.setCategorie(Utils.getCategoryFormBirthDate(membre.getBirth_date(), new Date()));
}
ls.logChange("Genre", target.getGenre(), membre.getGenre(), target);
target.setGenre(membre.getGenre());
ls.logChange("Email", target.getEmail(), membre.getEmail(), target);
target.setEmail(membre.getEmail());
return Panache.withTransaction(() -> repository.persist(target)).call(() -> ls.append());
})
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients,
SimpleCombModel.fromModel(membreModel)))
.call(membreModel -> (admin && membreModel.getUserId() != null) ?
((membreModel.getClub() != null) ?
keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) :
keycloakService.clearUser(membreModel.getUserId()))
: Uni.createFrom().nullItem())
.call(membreModel -> (membreModel.getUserId() != null) ?
keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem())
keycloakService.setAutoRoleMembre(membreModel.getUserId(), membreModel.getRole(),
membreModel.getGrade_arbitrage()) : Uni.createFrom().nullItem())
.call(membreModel -> (membreModel.getUserId() != null) ?
keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom()
.nullItem())
.call(membreModel -> {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_YEAR, -7);
Date dateLimit = calendar.getTime();
return competitionRepository.list("date > ?1", dateLimit)
.call(l -> l.isEmpty() ? Uni.createFrom().nullItem() :
Uni.join().all(l.stream().map(competitionModel ->
registerRepository.update(
"categorie = ?1, club = ?2 where competition = ?3 AND membre = ?4",
membreModel.getCategorie(), membreModel.getClub(), competitionModel,
membreModel)
).toList()).andFailFast());
})
.call(membreModel -> licenceRepository.update("club_id = ?1 where membre = ?2 AND saison = ?3",
(membreModel.getClub() == null) ? null : membreModel.getClub().getId(), membreModel,
Utils.getSaison()))
.call(membreModel -> (membre.getPhoto_data() != null && membre.getPhoto_data()
.size() > 0) ? ls.logAUpdate("Photo",
membreModel) : Uni.createFrom().nullItem())
.map(__ -> "OK");
}
public Uni<Long> add(FullMemberForm input) {
return clubRepository.findById(input.getClub())
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
.invoke(Unchecked.consumer(c -> {
if (c > 0 && input.getEmail() != null && !input.getEmail().isBlank())
throw new DBadRequestException(trad.t("email.deja.utilise"));
})))
.chain(clubModel -> {
MembreModel model = getMembreModel(input, clubModel);
return Panache.withTransaction(() -> repository.persist(model));
})
.call(membreModel -> ls.logAAdd(membreModel))
.invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients,
SimpleCombModel.fromModel(membreModel)))
.map(MembreModel::getId);
}
public Uni<Long> add(FullMemberForm input, String subject) {
return repository.find("userId = ?1", subject).firstResult()
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
.invoke(Unchecked.consumer(c -> {
if (c > 0) throw new DBadRequestException(trad.t("email.deja.utilise"));
})))
.call(membreModel ->
repository.count(
"unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2) AND club = ?3",
input.getLname(), input.getFname(), membreModel.getClub())
.invoke(Unchecked.consumer(c -> {
if (c > 0)
throw new DBadRequestException(trad.t("membre.deja.existent"));
})))
.chain(membreModel -> {
MembreModel model = getMembreModel(input, membreModel.getClub());
model.setRole(RoleAsso.MEMBRE);
model.setGrade_arbitrage(GradeArbitrage.NA);
return Panache.withTransaction(() -> repository.persist(model));
})
.call(membreModel -> ls.logAAdd(membreModel))
.invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients,
SimpleCombModel.fromModel(membreModel)))
.map(MembreModel::getId);
}
public Uni<String> delete(long id) {
return repository.findById(id)
.call(membreModel -> (membreModel.getUserId() != null) ?
keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem())
.call(membreModel -> ls.logADelete(membreModel))
.call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel)))
.invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id))
.map(__ -> "Ok");
}
public Uni<String> delete(long id, SecurityCtx securityCtx) {
return repository.findById(id)
.invoke(Unchecked.consumer(membreModel -> {
if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
throw new DForbiddenException();
}))
.invoke(Unchecked.consumer(membreModel -> {
if (membreModel.getLicence() != null) {
throw new DBadRequestException(
trad.t("membre.rm.err1"));
}
}))
.call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count()
.invoke(Unchecked.consumer(l -> {
if (l > 0)
throw new DBadRequestException(trad.t("membre.rm.err2"));
})))
.call(membreModel -> (membreModel.getUserId() != null) ?
keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem())
.call(membreModel -> ls.logADelete(membreModel))
.call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel)))
.invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id))
.call(__ -> Utils.deleteMedia(id, media, "ppMembre"))
.map(__ -> "Ok");
}
public Uni<?> setUserId(Long id, String id1) {
return repository.findById(id).chain(membreModel -> {
ls.logChange("KC UUID", membreModel.getUserId(), id1, membreModel);
membreModel.setUserId(id1);
return Panache.withTransaction(() -> repository.persist(membreModel));
return Panache.withTransaction(() -> repository.persist(membreModel))
.call(() -> ls.append());
});
}
private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) {
MembreModel model = new MembreModel();
model.setFname(Utils.formatPrenom(input.getFname()));
model.setLname(input.getLname().toUpperCase());
model.setEmail(input.getEmail());
model.setLicence(null);
model.setGenre(input.getGenre());
model.setCountry(input.getCountry());
model.setBirth_date(input.getBirth_date());
model.setCategorie(Utils.getCategoryFormBirthDate(input.getBirth_date(), new Date()));
model.setClub(clubModel);
model.setRole(input.getRole());
model.setGrade_arbitrage(input.getGrade_arbitrage());
return model;
}
public Uni<List<SimpleMembre>> getSimilar(String fname, String lname) {
return repository.listAll().map(membreModels -> membreModels.stream()
.filter(m -> StringSimilarity.similarity(m.getFname(), fname) <= 3 &&
StringSimilarity.similarity(m.getLname(), lname) <= 3)
.map(SimpleMembre::fromModel).toList());
}
public Uni<MeData> getMembre(String subject) {
MeData meData = new MeData();
return repository.find("userId = ?1", subject).firstResult()
.invoke(m -> meData.setMembre(m, trad))
.call(membreModel -> Mutiny.fetch(membreModel.getLicences())
.map(licences -> licences.stream().map(SimpleLicence::fromModel).toList())
.invoke(meData::setLicences))
.call(membreModel -> Mutiny.fetch(membreModel.getSelections())
.map(licences -> licences.stream().map(SimpleSelection::fromModel).toList())
.invoke(meData::setSelections))
.map(__ -> meData);
}
public Uni<Void> updateSettings(String subject, UserSettingForm settingForm) {
return repository.find("userId = ?1", subject).firstResult()
.invoke(Unchecked.consumer(membreModel -> {
if (settingForm.getResultPrivacy() != null) {
ls.logChange("Confidentialité des résultats", membreModel.getResultPrivacy(),
settingForm.getResultPrivacy(), membreModel);
membreModel.setResultPrivacy(settingForm.getResultPrivacy());
}
}))
.call(membreModel -> Panache.withTransaction(() -> repository.persist(membreModel)))
.call(membreModel -> ls.append())
.replaceWithVoid();
}
@Scheduled(cron = "0 0 1 1 9 ?")
Uni<Void> everySeason() {
return repository.list("birth_date IS NOT NULL")
.chain(l -> Uni.join().all(l.stream().map(m -> {
m.setCategorie(Utils.getCategoryFormBirthDate(m.getBirth_date(), new Date()));
return Panache.withTransaction(() -> repository.persist(m));
}).toList()).andCollectFailures())
.map(__ -> null);
}
}

View File

@@ -0,0 +1,235 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.LicenceModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@WithSession
@ApplicationScoped
public class PDFService {
private static final Logger LOGGER = Logger.getLogger(PDFService.class);
@Inject
CombRepository combRepository;
@Inject
ClubRepository clubRepository;
@ConfigProperty(name = "upload_dir")
String media;
@ConfigProperty(name = "pdf-maker.jar-path")
String pdfMakerJarPath;
@ConfigProperty(name = "pdf-maker.sign-file")
String sign_file;
@Inject
TradService trad;
public Uni<Response> getLicencePdf(String subject) {
return getLicencePdf(combRepository.find("userId = ?1", subject).firstResult()
.call(m -> Mutiny.fetch(m.getLicences())));
}
public Uni<Response> getLicencePdf(Uni<MembreModel> uniBase) {
return uniBase
.map(Unchecked.function(m -> {
LicenceModel licence = m.getLicences().stream()
.filter(licenceModel -> licenceModel.getSaison() == Utils.getSaison() && licenceModel.isValidate())
.findFirst()
.orElseThrow(() -> new DNotFoundException(trad.t("pas.de.licence.pour.la.saison.en.cours")));
try {
byte[] buff = make_pdf(m, licence);
if (buff == null)
throw new IOException("Error making pdf");
String mimeType = "application/pdf";
Response.ResponseBuilder resp = Response.ok(buff);
resp.type(MediaType.APPLICATION_OCTET_STREAM);
resp.header(HttpHeaders.CONTENT_LENGTH, buff.length);
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
resp.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; " + "filename=\"Attestation d'adhésion " + Utils.getSaison() + "-" +
(Utils.getSaison() + 1) + " de " + m.getLname() + " " + m.getFname() + ".pdf\"");
return resp.build();
} catch (Exception e) {
throw new IOException(e);
}
}));
}
private byte[] make_pdf(MembreModel m, LicenceModel licence) throws IOException, InterruptedException {
List<String> cmd = new ArrayList<>();
cmd.add("java");
cmd.add("-jar");
cmd.add(pdfMakerJarPath);
UUID uuid = UUID.randomUUID();
cmd.add("/tmp/" + uuid + ".pdf");
cmd.add("membre");
cmd.add(m.getFname());
cmd.add(m.getLname());
cmd.add(m.getGenre().str);
cmd.add(m.getCategorie().getName());
cmd.add(licence.getCertificate() == null ? "" : licence.getCertificate());
cmd.add(Utils.getSaison() + "");
cmd.add(m.getLicence() + "");
cmd.add(m.getClub().getName());
cmd.add(m.getClub().getNo_affiliation() + "");
cmd.add(m.getBirth_date() == null ? "--" : new SimpleDateFormat("dd/MM/yyyy").format(m.getBirth_date()));
FilenameFilter filter = (directory, filename) -> filename.startsWith(m.getId() + ".");
File[] files = new File(media, "ppMembre").listFiles(filter);
if (files != null && files.length > 0) {
File file = files[0];
cmd.add(file.getAbsolutePath());
} else {
cmd.add("/dev/null");
}
return getPdf(cmd, uuid);
}
public Uni<Response> getAffiliationPdf(String subject) {
return getAffiliationPdf(
combRepository.find("userId = ?1", subject).firstResult()
.invoke(Unchecked.consumer(m -> {
if (m == null || m.getClub() == null)
throw new DNotFoundException(trad.t("club.non.trouve"));
}))
.map(MembreModel::getClub)
.call(m -> Mutiny.fetch(m.getAffiliations())));
}
public Uni<Response> getAffiliationPdf(long id) {
return getAffiliationPdf(
clubRepository.findById(id)
.invoke(Unchecked.consumer(m -> {
if (m == null)
throw new DNotFoundException(trad.t("club.non.trouve"));
}))
.call(m -> Mutiny.fetch(m.getAffiliations())));
}
private Uni<Response> getAffiliationPdf(Uni<ClubModel> uniBase) {
return uniBase
.map(Unchecked.function(m -> {
if (m.getAffiliations().stream()
.noneMatch(licenceModel -> licenceModel.getSaison() == Utils.getSaison()))
throw new DNotFoundException(trad.t("pas.d.affiliation.pour.la.saison.en.cours"));
try {
byte[] buff = make_pdf(m);
if (buff == null)
throw new IOException("Error making pdf");
String mimeType = "application/pdf";
Response.ResponseBuilder resp = Response.ok(buff);
resp.type(MediaType.APPLICATION_OCTET_STREAM);
resp.header(HttpHeaders.CONTENT_LENGTH, buff.length);
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
resp.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; " + "filename=\"Attestation d'affiliation " + Utils.getSaison() + "-" +
(Utils.getSaison() + 1) + " de " + m.getName() + ".pdf\"");
return resp.build();
} catch (Exception e) {
throw new IOException(e);
}
}));
}
private byte[] make_pdf(ClubModel m) throws IOException, InterruptedException {
List<String> cmd = new ArrayList<>();
cmd.add("java");
cmd.add("-jar");
cmd.add(pdfMakerJarPath);
UUID uuid = UUID.randomUUID();
cmd.add("/tmp/" + uuid + ".pdf");
cmd.add("club");
cmd.add(m.getName());
cmd.add(Utils.getSaison() + "");
cmd.add(m.getNo_affiliation() + "");
cmd.add(new File(sign_file).getAbsolutePath());
return getPdf(cmd, uuid);
}
static byte[] getPdf(List<String> cmd, UUID uuid) throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder(cmd);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder builder = new StringBuilder();
Thread t = new Thread(() -> {
try {
String line;
while ((line = reader.readLine()) != null)
builder.append(line).append("\n");
} catch (Exception ignored) {
}
});
t.start();
int code = -1;
if (!process.waitFor(30, TimeUnit.SECONDS)) {
process.destroy();
builder.append("Timeout...");
} else {
code = process.exitValue();
}
if (t.isAlive())
t.interrupt();
PDFService.LOGGER.debug("PDF maker: " + builder);
if (code != 0) {
throw new IOException("Error code: " + code);
} else {
File file = new File("/tmp/" + uuid + ".pdf");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buff = fis.readAllBytes();
//noinspection ResultOfMethodCallIgnored
file.delete();
return buff;
} catch (IOException e) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
return null;
}
}
}

View File

@@ -0,0 +1,909 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.domain.entity.MatchModelExtend;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.Builder;
import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
@WithSession
@ApplicationScoped
public class ResultService {
@Inject
CompetitionRepository compRepository;
@Inject
RegisterRepository registerRepository;
@Inject
MembreService membreService;
@Inject
ClubRepository clubRepository;
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
CategoryRepository categoryRepository;
@Inject
MatchRepository matchRepository;
@Inject
CardRepository cardRepository;
@Inject
TradService trad;
@Inject
@CacheName("club-classement")
Cache cacheClubClassement;
private static final HashMap<String, Long> cacheClubClassementExp = new HashMap<>();
private static final HashMap<Long, String> combTempIds = new HashMap<>();
private static String getCombTempId(Long key) {
synchronized (combTempIds) {
if (!combTempIds.containsKey(key)) {
combTempIds.put(key, UUID.randomUUID().toString());
}
return combTempIds.get(key);
}
}
private static Long getCombTempId(String value) {
synchronized (combTempIds) {
for (Map.Entry<Long, String> entry : combTempIds.entrySet()) {
if (entry.getValue().equals(value)) {
return entry.getKey();
}
}
return null;
}
}
private static final HashMap<String, Long> clubTempIds = new HashMap<>();
private static Long getClubTempId(String key) {
synchronized (clubTempIds) {
if (!clubTempIds.containsKey(key)) {
clubTempIds.put(key, (System.currentTimeMillis() + clubTempIds.size()) * -1);
}
return clubTempIds.get(key);
}
}
private static String getClubTempId(Long value) {
synchronized (clubTempIds) {
for (Map.Entry<String, Long> entry : clubTempIds.entrySet()) {
if (entry.getValue().equals(value)) {
return entry.getKey();
}
}
return null;
}
}
public Uni<List<Object[]>> getList(SecurityCtx securityCtx) {
return membreService.getByAccountId(securityCtx.getSubject())
.chain(m -> registerRepository.list(
"membre = ?1 OR (TRUE = ?2 AND membre.club = ?3)",
m, securityCtx.isClubAdmin(), m.getClub()))
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().call(r -> Mutiny.fetch(r.getCompetition()))
.onItem().transform(RegisterModel::getCompetition)
.collect().asList()
.chain(l -> compRepository.list("owner = ?1 OR ?1 IN admin", securityCtx.getSubject())
.map(l2 -> Stream.concat(l.stream(), l2.stream()).distinct()
.map(c -> new Object[]{c.getUuid(), c.getName(), c.getDate()}).toList())
);
}
public Uni<HashMap<String, Long>> getCategoryList(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(__ -> getCategoryList(uuid));
}
public Uni<HashMap<String, Long>> getCategoryList(String uuid) {
return categoryRepository.list("compet.uuid = ?1", uuid)
.map(categoryModels -> {
HashMap<String, Long> map = new HashMap<>();
categoryModels.stream()
.sorted(Comparator.comparing(CategoryModel::getName))
.forEachOrdered(categoryModel -> map.put(categoryModel.getName(), categoryModel.getId()));
return map;
});
}
public Uni<ResultCategoryData> getCategory(String uuid, long poule, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(membreModel -> getData(uuid, poule, membreModel));
}
public Uni<ResultCategoryData> getCategory(String uuid, long poule) {
return getData(uuid, poule, null);
}
private Uni<ResultCategoryData> getData(String uuid, long poule, MembreModel membreModel) {
List<CardModel> cards = new ArrayList<>();
return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
.call(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
Mutiny.fetch(list.get(0).getCategory().getTree()))
.chain(list -> cardRepository.list("competition.uuid = ?1", uuid).invoke(cards::addAll)
.map(c -> list.stream().map(m -> new MatchModelExtend(m, c)).toList()))
.map(matchModels -> {
ResultCategoryData out = new ResultCategoryData();
CategoryModel categoryModel = matchModels.get(0).getCategory();
out.setName(categoryModel.getName());
out.setType(categoryModel.getType());
out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName()
.split(";"));
out.setGenTime(System.currentTimeMillis());
out.setTreeIsClassement(categoryModel.isTreeAreClassement());
getArray2(matchModels, membreModel, out);
getTree(categoryModel.getTree(), membreModel, cards, out);
getClassementArray(categoryModel, membreModel, cards, out);
return out;
});
}
public void getArray2(List<MatchModelExtend> matchModels_, MembreModel membreModel, ResultCategoryData out) {
List<MatchModelExtend> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList();
HashMap<Character, List<MatchModelExtend>> matchMap = new HashMap<>();
for (MatchModelExtend model : matchModels) {
char g = model.getPoule();
if (!matchMap.containsKey(g))
matchMap.put(g, new ArrayList<>());
matchMap.get(g).add(model);
}
matchMap.forEach((c, matchEntities) -> {
List<ResultCategoryData.PouleArrayData> matchs = matchEntities.stream()
.sorted(Comparator.comparing(MatchModelExtend::getCategory_ord))
.map(o -> ResultCategoryData.PouleArrayData.fromModel(o, membreModel,
ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))
.toList();
List<ResultCategoryData.RankArray> rankArray = matchEntities.stream()
.flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id(), m.getC1_guest(), m.getC2_guest()))
.distinct()
.filter(Objects::nonNull)
.map(comb -> {
CombStat stat = makeStat(matchEntities, comb);
return new ResultCategoryData.RankArray(0, comb,
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w,
stat.pointMake, stat.pointTake, stat.getPointRate());
})
.filter(r -> r.getPointMake() > 0 || r.getPointTake() > 0)
.sorted(Comparator
.comparing(ResultCategoryData.RankArray::getScore)
.thenComparing(ResultCategoryData.RankArray::getWin)
.thenComparing(ResultCategoryData.RankArray::getPointRate).reversed())
.toList();
out.getMatchs().put(c, matchs);
int lastScore = -1;
int lastWin = -1;
float pointRate = 0;
int rank = 0;
for (ResultCategoryData.RankArray rankArray1 : rankArray) {
if (rankArray1.getScore() != lastScore || rankArray1.getWin() != lastWin || pointRate != rankArray1.getPointRate()) {
lastScore = rankArray1.getScore();
lastWin = rankArray1.getWin();
pointRate = rankArray1.getPointRate();
rank++;
}
rankArray1.setRank(rank);
}
out.getRankArray().put(c, rankArray);
});
}
public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> cards,
ResultCategoryData out) {
if ((categoryModel.getType() & 2) != 0) {
AtomicInteger rank = new AtomicInteger(0);
categoryModel.getTree().stream()
.filter(t -> t.getLevel() > 0)
.sorted(Comparator.comparing(TreeModel::getLevel))
.forEach(treeModel -> makeClassementRow(membreModel,
new MatchModelExtend(treeModel.getMatch(), cards), out, rank));
categoryModel.getTree().stream()
.filter(t -> t.getLevel() <= -10)
.sorted(Comparator.comparing(TreeModel::getLevel).reversed())
.forEach(treeModel -> makeClassementRow(membreModel,
new MatchModelExtend(treeModel.getMatch(), cards), out, rank));
} else {
for (List<ResultCategoryData.RankArray> list : out.getRankArray().values()) {
for (ResultCategoryData.RankArray r : list) {
out.getClassement()
.add(new ResultCategoryData.ClassementData(r.getRank(), r.getComb(), r.getName()));
}
}
}
}
private static void makeClassementRow(MembreModel membreModel, MatchModelExtend m, ResultCategoryData out,
AtomicInteger rank) {
if (m.isEnd()) {
if (m.getWin() > 0) {
out.getClassement()
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
out.getClassement()
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC2(),
m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
} else if (m.getWin() < 0) {
out.getClassement()
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC2(),
m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
out.getClassement()
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
} else {
out.getClassement()
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
out.getClassement()
.add(new ResultCategoryData.ClassementData(rank.get(), m.getC2(),
m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
}
} else {
out.getClassement().add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), null, null));
out.getClassement().add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), null, null));
}
}
private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst, MembreModel membreModel,
ResultPrivacy privacy, List<CardModel> cards) {
dst.setData(
ResultCategoryData.TreeData.from(new MatchModelExtend(src.getMatch(), cards), src.getLevel(),
membreModel, privacy));
if (src.getLeft() != null) {
dst.setLeft(new TreeNode<>());
convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy, cards);
}
if (src.getRight() != null) {
dst.setRight(new TreeNode<>());
convertTree(src.getRight(), dst.getRight(), membreModel, privacy, cards);
}
}
private void getTree(List<TreeModel> treeModels, MembreModel membreModel, List<CardModel> cards,
ResultCategoryData out) {
ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
treeModels.stream()
.filter(t -> t.getLevel() != 0)
.sorted(Comparator.comparing(TreeModel::getLevel))
.forEach(treeModel -> {
TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
convertTree(treeModel, root, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS, cards);
trees.add(root);
});
out.setTrees(trees);
}
public Uni<CombsArrayData> getAllCombArray(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(membreModel -> getAllCombArray_(uuid, membreModel));
}
public Uni<CombsArrayData> getAllCombArrayPublic(String uuid) {
return getAllCombArray_(uuid, null);
}
private Uni<CombsArrayData> getAllCombArray_(String uuid, MembreModel membreModel) {
return registerRepository.list("competition.uuid = ?1", uuid)
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
.map(cards -> new Pair<>(registers,
matchModels.stream().map(m -> new MatchModelExtend(m, cards)).toList()))))
.map(pair -> {
List<RegisterModel> registers = pair.getKey();
List<MatchModelExtend> matchModels = pair.getValue();
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
List<CombsArrayData.CombsData> combs = matchModels.stream()
.flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id(), m.getC1_guest(), m.getC2_guest()))
.filter(Objects::nonNull)
.distinct()
.map(comb -> {
var builder2 = CombsArrayData.CombsData.builder();
CombStat stat = makeStat(matchModels, comb);
Categorie categorie = null;
String clubName = null;
Optional<RegisterModel> register = registers.stream()
.filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
if (register.isPresent()) {
categorie = register.get().getCategorie2();
ClubModel club = register.get().getClub2();
clubName = (club == null) ? trad.t("no.licence") : club.getName();
} else if (comb instanceof CompetitionGuestModel guestModel) {
categorie = guestModel.getCategorie();
clubName = guestModel.getClub();
} else if (comb instanceof MembreModel model) {
categorie = model.getCategorie();
clubName = (model.getClub() == null) ? trad.t("no.licence")
: model.getClub().getName();
}
builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
builder2.w(stat.w);
builder2.l(stat.l);
builder2.ratioVictoire((stat.l == 0) ? stat.w : (float) stat.w / stat.l);
builder2.club(clubName);
builder2.pointMake(stat.pointMake);
builder2.pointTake(stat.pointTake);
builder2.ratioPoint(stat.getPointRate());
return builder2.build();
})
.sorted(Comparator.comparing(CombsArrayData.CombsData::name))
.toList();
builder.nb_insc(combs.size());
builder.tt_match((int) matchModels.stream().filter(MatchModelExtend::isEnd).count());
builder.point(matchModels.stream()
.filter(MatchModelExtend::isEnd)
.flatMap(m -> m.getScoresToCompute().stream())
.mapToInt(s -> s.getS1() + s.getS2()).sum());
builder.combs(combs);
return builder.build();
});
}
public Uni<HashMap<String, String>> getCombList(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(membreModel -> getCombList(uuid, ResultPrivacy.REGISTERED_ONLY));
}
public Uni<HashMap<String, String>> getCombList(String uuid) {
return getCombList(uuid, ResultPrivacy.PUBLIC);
}
private Uni<HashMap<String, String>> getCombList(String uuid, ResultPrivacy privacy) {
return registerRepository.list("competition.uuid = ?1 AND membre.resultPrivacy <= ?2", uuid, privacy)
.map(models -> {
HashMap<String, String> map = new HashMap<>();
models.forEach(
r -> map.put(r.getMembre().getName(), getCombTempId(r.getMembre().getId())));
return map;
})
.chain(map -> competitionGuestRepository.list("competition.uuid = ?1 AND lname != \"__team\"", uuid)
.map(models -> {
models.forEach(guestModel -> map.put(guestModel.getName(),
getCombTempId(guestModel.getId() * -1)));
return map;
})
);
}
public Uni<?> getCombArrayPublic(String uuid, String combTempId, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(membreModel -> getCombArrayPublic(uuid, combTempId, ResultPrivacy.REGISTERED_ONLY));
}
public Uni<?> getCombArrayPublic(String uuid, String combTempId) {
return getCombArrayPublic(uuid, combTempId, ResultPrivacy.PUBLIC);
}
private Uni<?> getCombArrayPublic(String uuid, String combTempId, ResultPrivacy privacy) {
CombArrayData.CombArrayDataBuilder builder = CombArrayData.builder();
Long id = getCombTempId(combTempId);
if (id == null) {
return Uni.createFrom().failure(new DForbiddenException(trad.t("comb.not.found")));
}
Uni<List<MatchModelExtend>> uni;
if (id >= 0) {
uni = registerRepository.find("membre.id = ?1 AND competition.uuid = ?2 AND membre.resultPrivacy <= ?3", id,
uuid, privacy).firstResult()
.chain(Unchecked.function(registerModel -> {
if (registerModel == null)
throw new DBadRequestException(trad.t("combattant.non.inscrit"));
builder.name(Utils.getFullName(registerModel.getMembre()));
builder.club((registerModel.getClub2() == null) ? trad.t("no.licence") :
registerModel.getClub2().getName());
builder.cat((registerModel.getCategorie2() == null) ? "---" :
registerModel.getCategorie2().getName(trad));
return matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " +
"WHERE m.category.compet.uuid = ?1 AND (m.c1_id = ?2 OR m.c2_id = ?2 OR c1g = ?2 OR c2g = ?2)",
uuid, registerModel.getMembre())
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
.map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards))
.toList()));
}));
} else {
uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult()
.chain(guestModel -> {
builder.name(Utils.getFullName(guestModel));
builder.club(guestModel.getClub());
builder.cat(
(guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie().getName(trad));
return matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
"WHERE m.category.compet.uuid = ?1 AND (m.c1_guest = ?2 OR m.c2_guest = ?2 OR c1g = ?2 OR c2g = ?2)",
uuid, guestModel)
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
.map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards))
.toList()));
});
}
return uni.invoke(matchModels -> {
List<CategoryModel> pouleModels = matchModels.stream().map(MatchModelExtend::getCategory).distinct()
.toList();
List<CombArrayData.MatchsData> matchs = new ArrayList<>();
AtomicInteger sumW = new AtomicInteger();
AtomicInteger sumPointMake = new AtomicInteger(0);
AtomicInteger sumPointTake = new AtomicInteger(0);
for (MatchModelExtend matchModel : matchModels) {
if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) ||
(matchModel.getC2_id() == null && matchModel.getC2_guest() == null))
continue;
var builder2 = CombArrayData.MatchsData.builder();
builder2.date(matchModel.getDate());
builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory()))
.map(CategoryModel::getName).findFirst().orElse(""));
builder2.end(matchModel.isEnd());
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
if (matchModel.isC1(id)) {
builder2.adv(matchModel.getC2Name());
if (matchModel.isEnd()) {
matchModel.getScoresToCompute()
.forEach(scoreEntity -> {
pointMake.addAndGet(scoreEntity.getS1());
pointTake.addAndGet(scoreEntity.getS2());
});
builder2.score(matchModel.getScoresToPrint().stream()
.map(s -> new Integer[]{s.getS1(), s.getS2()}).toList());
} else {
builder2.score(new ArrayList<>());
}
builder2.win(matchModel.isEnd() && matchModel.getWin() > 0);
} else {
builder2.adv(matchModel.getC1Name());
if (matchModel.isEnd()) {
matchModel.getScoresToCompute()
.forEach(scoreEntity -> {
pointMake.addAndGet(scoreEntity.getS2());
pointTake.addAndGet(scoreEntity.getS1());
});
builder2.score(matchModel.getScoresToPrint().stream()
.map(s -> new Integer[]{s.getS2(), s.getS1()}).toList());
} else {
builder2.score(new ArrayList<>());
}
builder2.win(matchModel.isEnd() && matchModel.getWin() < 0);
}
builder2.eq(matchModel.isEnd() && matchModel.getWin() == 0);
builder2.ratio(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
sumPointMake.addAndGet(pointMake.get());
sumPointTake.addAndGet(pointTake.get());
matchs.add(builder2.build());
if (builder2.win)
sumW.getAndIncrement();
}
builder.totalWin(sumW.get());
builder.pointRatio(
(sumPointTake.get() == 0) ? sumPointMake.get() : (float) sumPointMake.get() / sumPointTake.get());
builder.pointMake(sumPointMake.get());
builder.pointTake(sumPointTake.get());
matchs.sort(Comparator.comparing(CombArrayData.MatchsData::poule)
.thenComparing(CombArrayData.MatchsData::adv));
builder.matchs(matchs);
})
.map(__ -> builder.build());
}
@Builder
@RegisterForReflection
public static record CombsArrayData(int nb_insc, int tt_match, long point, List<CombsData> combs) {
@Builder
@RegisterForReflection
public static record CombsData(String cat, String club, String name, int w, int l, float ratioVictoire,
float ratioPoint, int pointMake, int pointTake) {
}
}
@Builder
@RegisterForReflection
public static record CombArrayData(String name, String club, String cat, int totalWin,
float pointRatio, int pointMake, int pointTake, List<MatchsData> matchs) {
@Builder
@RegisterForReflection
public static record MatchsData(Date date, String poule, String adv, List<Integer[]> score, float ratio,
boolean win, boolean eq, boolean end) {
}
}
public Uni<HashMap<String, Long>> getClubList(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(__ -> getClubList(uuid));
}
public Uni<HashMap<String, Long>> getClubList(String uuid) {
return registerRepository.list("competition.uuid = ?1", uuid)
.map(registers -> {
HashMap<String, Long> registerMap = new HashMap<>();
registers.stream().map(RegisterModel::getClub2).distinct().filter(Objects::nonNull)
.forEach(registerClub -> registerMap.put(registerClub.getName(), registerClub.getId()));
return registerMap;
})
.chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid)
.map(guests -> {
guests.stream().map(CompetitionGuestModel::getClub).distinct()
.filter(Objects::nonNull)
.forEach(guestClub -> map.putIfAbsent(guestClub,
getClubTempId(guestClub)));
return map;
})
);
}
public Uni<ClubArrayData> getClubArray(String uuid, Long id, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(membreModel -> getClubArray2(uuid, id, membreModel));
}
public Uni<ClubArrayData> getClubArray2(String uuid, Long id, MembreModel membreModel) {
if (id < 0) {
String clubName = getClubTempId(id);
if (clubName == null) {
return Uni.createFrom().failure(new DForbiddenException(trad.t("club.not.found")));
}
return competitionGuestRepository.list("competition.uuid = ?1 AND club = ?2", uuid, clubName)
.call(list -> {
if (list.isEmpty())
return Uni.createFrom().failure(new DBadRequestException(trad.t("club.not.found")));
return Uni.createFrom().voidItem();
})
.chain(guests -> matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
"WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
uuid, guests)
.chain(mm -> cardRepository.list("competition.uuid = ?1", uuid)
.map(cards ->
getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(),
mm.stream().map(m -> new MatchModelExtend(m, cards)).toList(),
new ArrayList<>(), membreModel)
)));
} else {
return cardRepository.list("competition.uuid = ?1", uuid)
.chain(cards -> clubRepository.findById(id).chain(clubModel ->
registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
.chain(registers -> matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " +
"WHERE m.category.compet.uuid = ?1 AND (m.c1_id IN ?2 OR m.c2_id IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
uuid, registers.stream().map(RegisterModel::getMembre).toList())
.map(matchModels -> matchModels.stream()
.map(m -> new MatchModelExtend(m, cards)))
.chain(s -> competitionGuestRepository.list(
"competition.uuid = ?1 AND club = ?2", uuid, clubModel.getName())
.chain(guests -> matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
"WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
uuid, guests)
.map(mm -> new Pair<>(guests, Stream.concat(s, mm.stream()
.map(m -> new MatchModelExtend(m, cards)))
.distinct().toList()))
))
.map(p ->
getClubArray2(clubModel.getName(),
Stream.concat(
registers.stream().map(RegisterModel::getMembre),
p.getKey().stream()
).toList(),
p.getValue(), registers, membreModel)
))));
}
}
private ClubArrayData getClubArray2(String name, List<CombModel> combs, List<MatchModelExtend> matchModels,
List<RegisterModel> registers, MembreModel membreModel) {
ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();
builder.name(name);
builder.nb_insc(combs.size());
ArrayList<Long> win_ids = new ArrayList<>();
ArrayList<Long> match_ids = new ArrayList<>();
List<ClubArrayData.CombData> combData = combs.stream().map(comb -> {
var builder2 = ClubArrayData.CombData.builder();
CombStat stat = makeStat(matchModels, comb);
Categorie categorie = null;
Optional<RegisterModel> register = registers.stream()
.filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
if (register.isPresent()) {
categorie = register.get().getCategorie2();
} else if (comb instanceof CompetitionGuestModel guestModel) {
categorie = guestModel.getCategorie();
} else if (comb instanceof MembreModel model) {
categorie = model.getCategorie();
}
builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
builder2.w(stat.w);
builder2.l(stat.l);
builder2.ratioVictoire((stat.l == 0) ? stat.w : (float) stat.w / stat.l);
builder2.pointMake(stat.pointMake);
builder2.pointTake(stat.pointTake);
builder2.ratioPoint(stat.getPointRate());
win_ids.addAll(stat.win_ids);
match_ids.addAll(stat.match_ids);
return builder2.build();
})
.sorted(Comparator.comparing(ClubArrayData.CombData::name))
.toList();
builder.nb_match((int) match_ids.stream().distinct().count());
builder.match_w((int) win_ids.stream().distinct().count());
builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0)
.mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L));
builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum());
builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum());
builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0)
.mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L));
builder.combs(combData);
return builder.build();
}
private static CombStat makeStat(List<MatchModelExtend> matchModels, CombModel comb) {
CombStat stat = new CombStat();
matchModels.stream()
.filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
.forEach(matchModel -> {
stat.match_ids.add(matchModel.getId());
int win = matchModel.getWin();
if (win == 0) {
stat.score += 1;
} else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) {
stat.w++;
stat.win_ids.add(matchModel.getId());
stat.score += 2;
} else {
stat.l++;
}
matchModel.getScoresToCompute()
.forEach(score -> {
if (matchModel.isC1(comb)) {
stat.pointMake += score.getS1();
stat.pointTake += score.getS2();
} else {
stat.pointMake += score.getS2();
stat.pointTake += score.getS1();
}
});
});
return stat;
}
@Builder
@RegisterForReflection
public static record ClubArrayData(String name, int nb_insc, int nb_match, int match_w, float ratioVictoire,
float ratioPoint, int pointMake, int pointTake, List<CombData> combs) {
@Builder
@RegisterForReflection
public static record CombData(String cat, String name, int w, int l, float ratioVictoire,
float ratioPoint, int pointMake, int pointTake) {
}
}
public Uni<List<ClubClassement>> getAllClubArray(String uuid) {
return getAllClubArray(uuid, true);
}
public Uni<List<ClubClassement>> getAllClubArray(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(membreModel -> getAllClubArray(uuid, true));
}
public Uni<List<ClubClassement>> getAllClubArray(String uuid, boolean cache) {
List<CardModel> cards = new java.util.ArrayList<>();
return (!cache || cacheClubClassementExp.getOrDefault(uuid, 0L) < System.currentTimeMillis() ?
cacheClubClassement.invalidate(uuid).invoke(() -> cacheClubClassementExp.remove(uuid)) :
Uni.createFrom().voidItem()
).chain(o -> cacheClubClassement.getAsync(uuid, k -> cardRepository.list("competition.uuid = ?1", uuid)
.invoke(__ -> System.out.println("Cache miss for club classement with uuid " + uuid))
.invoke(cards::addAll)
.chain(__ -> matchRepository.list("category.compet.uuid = ?1", uuid))
.chain(matchs -> {
HashMap<CategoryModel, List<MatchModel>> map = new HashMap<>();
for (MatchModel match : matchs) {
if (!map.containsKey(match.getCategory()))
map.put(match.getCategory(), new java.util.ArrayList<>());
map.get(match.getCategory()).add(match);
}
return Multi.createFrom().iterable(map.entrySet())
.onItem().call(entry -> Mutiny.fetch(entry.getKey().getTree()))
.map(entry -> {
ResultCategoryData tmp = new ResultCategoryData();
getArray2(entry.getValue().stream().map(m -> new MatchModelExtend(m, cards))
.toList(),
null, tmp);
getClassementArray(entry.getKey(), null, cards, tmp);
return tmp;
})
.collect().asList();
})
.map(categoryData -> {
HashMap<String, ClubClassement> clubMap = new HashMap<>();
categoryData.forEach(
c -> c.getClassement().stream().map(ResultCategoryData.ClassementData::comb)
.filter(Objects::nonNull)
.distinct()
.map(comb -> {
if (comb instanceof MembreModel membreModel2) {
return (membreModel2.getClub() != null) ? membreModel2.getClub()
.getName() : "";
} else if (comb instanceof CompetitionGuestModel guestModel) {
return guestModel.getClub();
}
return "";
})
.filter(s -> s != null && !s.isBlank() && !s.equals("Team"))
.distinct()
.forEach(clubName -> clubMap.putIfAbsent(clubName,
new ClubClassement(clubName))));
categoryData.forEach(c -> c.getClassement().forEach(classementData -> {
if (classementData.rank() > 3)
return;
if (classementData.comb() != null) {
String clubName = "";
if (classementData.comb() instanceof MembreModel membreModel2) {
clubName = (membreModel2.getClub() != null) ? membreModel2.getClub()
.getName() : "";
} else if (classementData.comb() instanceof CompetitionGuestModel guestModel) {
clubName = guestModel.getClub();
}
if (clubName != null && !clubName.isBlank()
&& !clubName.equals("Team") && clubMap.containsKey(clubName)) {
ClubClassement entity = clubMap.get(clubName);
entity.score[classementData.rank() - 1]++;
entity.tt_score += 4 - classementData.rank();
}
}
}));
return clubMap.values().stream()
.sorted(Comparator.comparingInt((ClubClassement c) -> c.tt_score)
.thenComparingInt(c -> c.score[0])
.thenComparingInt(c -> c.score[1])
.thenComparingInt(c -> c.score[2]).reversed())
.toList();
})
.invoke(__ -> cacheClubClassementExp.put(uuid, System.currentTimeMillis() + 60 * 1000L))
));
}
@Data
@RegisterForReflection
public static class ClubClassement {
String name;
Integer[] score = new Integer[]{0, 0, 0};
int tt_score = 0;
public ClubClassement(String name) {
this.name = name;
}
}
@RegisterForReflection
public static class CombStat {
public int w;
public int l;
public int score;
public int pointMake;
public int pointTake;
public ArrayList<Long> win_ids = new ArrayList<>();
public ArrayList<Long> match_ids = new ArrayList<>();
public CombStat() {
this.w = 0;
this.l = 0;
this.score = 0;
this.pointMake = 0;
this.pointTake = 0;
}
public float getPointRate() {
return (pointTake == 0) ? pointMake : (float) pointMake / pointTake;
}
}
private Uni<MembreModel> hasAccess(String uuid, SecurityCtx securityCtx) {
return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid)
.firstResult()
.chain(Unchecked.function(o -> {
if (o != null)
return Uni.createFrom().item(o.getMembre());
return membreService.getByAccountId(securityCtx.getSubject()).chain(m -> {
if (securityCtx.isClubAdmin()) {
return registerRepository.count("membre.club = ?2 AND competition.uuid = ?1",
uuid, m.getClub()).chain(c -> {
if (c > 0) return Uni.createFrom().item(m);
return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)",
uuid, securityCtx.getSubject())
.chain(c2 -> {
if (c2 > 0) return Uni.createFrom().item(m);
return Uni.createFrom().failure(new DForbiddenException(
trad.t("access.denied")));
});
});
} else {
return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)", uuid,
securityCtx.getSubject())
.chain(c2 -> {
if (c2 > 0) return Uni.createFrom().item(m);
return Uni.createFrom()
.failure(new DForbiddenException(trad.t("access.denied")));
});
}
});
}));
}
}

View File

@@ -0,0 +1,64 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.LogModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.model.SelectionModel;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.data.repository.SelectionRepository;
import fr.titionfire.ffsaf.rest.data.SimpleSelection;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.List;
import java.util.function.Consumer;
@WithSession
@ApplicationScoped
public class SelectionService {
@Inject
CombRepository combRepository;
@Inject
SelectionRepository repository;
@Inject
LoggerService ls;
public Uni<List<SelectionModel>> getSelection(long id, Consumer<MembreModel> checkPerm) {
return combRepository.findById(id).invoke(checkPerm)
.chain(combRepository -> Mutiny.fetch(combRepository.getSelections()));
}
public Uni<SelectionModel> setSelection(long id, SimpleSelection data) {
if (data.getId() == -1) {
return combRepository.findById(id).chain(membreModel -> {
SelectionModel model = new SelectionModel();
model.setMembre(membreModel);
model.setSaison(data.getSaison());
model.setCategorie(data.getCategorie());
return Panache.withTransaction(() -> repository.persist(model))
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(),
licenceModel));
});
} else {
return repository.findById(data.getId()).chain(model -> {
ls.logChange("Catégorie", model.getCategorie(), data.getCategorie(), model);
model.setCategorie(data.getCategorie());
return Panache.withTransaction(() -> repository.persist(model))
.call(__ -> ls.append());
});
}
}
public Uni<Void> deleteSelection(long id) {
return repository.findById(id)
.call(model -> ls.logADelete(model))
.chain(model -> Panache.withTransaction(() -> repository.delete(model)));
}
}

View File

@@ -0,0 +1,71 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
import fr.titionfire.ffsaf.rest.data.LicenceStats;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.concurrent.ConcurrentHashMap;
@WithSession
@ApplicationScoped
public class StatsService {
@Inject
LicenceRepository licenceRepository;
public Uni<LicenceStats> getStats() {
ConcurrentHashMap<Categorie, Integer> categories = new ConcurrentHashMap<>();
LicenceStats stats = new LicenceStats();
int currentSaison = Utils.getSaison();
//noinspection ReactiveStreamsUnusedPublisher
return licenceRepository.listAll()
.onItem().transformToMulti(licences -> Multi.createFrom().iterable(licences))
.call(licence -> Mutiny.fetch(licence.getMembre()))
.invoke(licence -> {
if (!stats.getLicences().containsKey(licence.getSaison()))
stats.getLicences().put(licence.getSaison(), new LicenceStats.YearStats());
LicenceStats.YearStats yearStats = stats.getLicences().get(licence.getSaison());
// System.out.println("stats: " + licence.getMembre().getFname());
if (licence.isValidate()) {
if (licence.getMembre().getGenre() == Genre.H)
yearStats.addH();
else if (licence.getMembre().getGenre() == Genre.F)
yearStats.addF();
else
yearStats.addNa();
if (licence.getSaison() == currentSaison && licence.getMembre().getCategorie() != null) {
categories.put(licence.getMembre().getCategorie(),
categories.getOrDefault(licence.getMembre().getCategorie(), 0) + 1);
}
} else {
yearStats.addNotValid();
}
})
.collect().asList()
.map(__ -> {
for (Categorie c : Categorie.values()) {
LicenceStats.CategoriesStats o = new LicenceStats.CategoriesStats();
o.setName(c.getName());
o.setCount(categories.getOrDefault(c, 0));
stats.getCategories().add(o);
}
return stats;
});
}
}

View File

@@ -0,0 +1,52 @@
package fr.titionfire.ffsaf.domain.service;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
@RequestScoped
public class TradService {
@Inject
Instance<ContainerRequestContext> requestContextInstance;
public static final Locale fallbackLocale = Locale.FRANCE;
public String t(String key) {
return translate(key);
}
public String t(String key, WebSocketConnection connection) {
List<String> lang = connection.handshakeRequest().headers().get("Accept-Language");
Locale userLocale = lang != null && !lang.isEmpty() ? Locale.forLanguageTag(lang.get(0)) : fallbackLocale;
return translate(key, userLocale);
}
public String translate(String key) {
ContainerRequestContext requestContext = requestContextInstance.get();
Locale userLocale = (Locale) requestContext.getProperty("userLocale");
return translate(key, userLocale);
}
public String translate(String key, Locale userLocale) {
try {
ResourceBundle messages = ResourceBundle.getBundle("lang.messages", userLocale);
return messages.getString(key);
} catch (MissingResourceException e) {
try {
ResourceBundle fallbackMessages = ResourceBundle.getBundle("lang.messages", fallbackLocale);
return fallbackMessages.getString(key);
} catch (MissingResourceException ex) {
return key;
}
}
}
}

View File

@@ -0,0 +1,23 @@
package fr.titionfire.ffsaf.domain.service;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.HashMap;
@ApplicationScoped
public class UpdateService { // For public result page
static HashMap<Long, Long> lastUpdate = new HashMap<>();
public void setNewData(long id) {
lastUpdate.put(id, System.currentTimeMillis());
}
public boolean needUpdate(long id, long last_update) {
if (!lastUpdate.containsKey(id)) {
lastUpdate.put(id, System.currentTimeMillis() - 5000);
return true;
}
return lastUpdate.getOrDefault(id, 0L) > last_update;
}
}

View File

@@ -0,0 +1,28 @@
package fr.titionfire.ffsaf.domain.service;
import io.quarkiverse.antivirus.runtime.Antivirus;
import io.quarkiverse.antivirus.runtime.AntivirusScanResult;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.io.InputStream;
import java.util.List;
@ApplicationScoped
public class VirusScannerService {
@Inject
Antivirus antivirus;
public Uni<List<AntivirusScanResult>> scanFileReactive(String fileName, InputStream inputStream) {
System.out.println("Starting reactive virus scan for file: " + fileName);
// Wrap the blocking antivirus scan in a reactive context
// This moves the blocking operation to a worker thread
return Uni.createFrom().item(() -> {
System.out.println("Scanning file on worker thread: " + fileName);
return antivirus.scan(fileName, inputStream);
}).runSubscriptionOn(io.smallrye.mutiny.infrastructure.Infrastructure.getDefaultWorkerPool());
}
}

View File

@@ -0,0 +1,40 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.rest.client.dto.HelloassoNotification;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@ApplicationScoped
public class WebhookService {
@Inject
CheckoutService checkoutService;
@Inject
CompetitionService competitionService;
@ConfigProperty(name = "helloasso.organizationSlug")
String organizationSlug;
public Uni<Response> helloAssoNotification(HelloassoNotification notification) {
if (notification.getEventType().equals("Payment")) {
if (notification.getData().getOrder().getFormType().equals("Checkout")) {
if (notification.getData().getOrder().getOrganizationSlug().equalsIgnoreCase(organizationSlug)) {
return checkoutService.paymentStatusChange(notification.getData().getState(),
notification.getMetadata());
}
} else if (notification.getData().getOrder().getFormType().equals("Event")) {
return competitionService.unregisterHelloAsso(notification.getData());
}
}else if (notification.getEventType().equals("Order")){
if (notification.getData().getFormType().equals("Event")) {
return competitionService.registerHelloAsso(notification.getData());
}
}
return Uni.createFrom().item(Response.ok().build());
}
}

View File

@@ -40,7 +40,7 @@ public class Client_Thread extends Thread {
private boolean isAuth;
private final HashMap<UUID, JsonConsumer<Object>> waitResult = new HashMap<>();
private final HashMap<UUID, JsonConsumer<?>> waitResult = new HashMap<>();
public Client_Thread(ServerCustom serv, Socket s, PublicKey publicKey) throws IOException {
this.serv = serv;
@@ -162,7 +162,7 @@ public class Client_Thread extends Thread {
sendReq(object, type, null);
}
public void sendReq(Object object, String code, JsonConsumer<Object> consumer) {
public void sendReq(Object object, String code, JsonConsumer<?> consumer) {
UUID uuid;
do {
uuid = UUID.randomUUID();

View File

@@ -22,6 +22,7 @@ public class SimpleClubModel {
if (model == null)
return null;
return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(), model.getShieldURL());
return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(),
"/api/club/" + model.getClubId() + "/logo");
}
}

View File

@@ -8,12 +8,14 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Schema(hidden = true)
public class SimpleCombModel {
Long id;
String lname = "";
@@ -30,6 +32,6 @@ public static SimpleCombModel fromModel(MembreModel model) {
return new SimpleCombModel(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
(model.getClub() == null) ? null : SimpleClubModel.fromModel(model.getClub()),
model.getGenre(), model.getLicence(), model.getCountry());
model.getGenre(), (model.getLicence() == null) ? -1 : model.getLicence(), model.getCountry());
}
}

View File

@@ -0,0 +1,10 @@
package fr.titionfire.ffsaf.net2.data;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.util.List;
import java.util.UUID;
@RegisterForReflection
public record SimpleCompet(long id, String owner, boolean show_blason, boolean show_flag, List<UUID> admin, List<UUID> table) {
}

View File

@@ -11,16 +11,6 @@ import java.util.HashMap;
public class RComb {
private static final Logger LOGGER = Logger.getLogger(RComb.class);
final IAction findComb = (client_Thread, message) -> {
try {
SimpleCombModel combModel = ServerCustom.getInstance().membreService.find(message.data().get("licence").asInt(), message.data().get("np").asText());
client_Thread.sendRepTo(combModel, message);
} catch (Throwable e) {
LOGGER.error(e.getMessage(), e);
client_Thread.sendErrTo(e.getMessage(), message);
}
};
final CIA<Long> findByIdOptionalComb = new CIA<>(Long.class, (client_Thread, message) -> {
try {
SimpleCombModel combModel = ServerCustom.getInstance().membreService.findByIdOptionalComb(message.data());
@@ -34,7 +24,6 @@ public class RComb {
public static void register(HashMap<String, IAction> iMap) {
RComb rComb = new RComb();
iMap.put("findComb", rComb.findComb);
iMap.put("findByIdOptionalComb", rComb.findByIdOptionalComb);
}
}

View File

@@ -0,0 +1,40 @@
package fr.titionfire.ffsaf.net2.request;
import fr.titionfire.ffsaf.net2.Client_Thread;
import fr.titionfire.ffsaf.net2.data.SimpleCompet;
import fr.titionfire.ffsaf.utils.JsonConsumer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture;
public class SReqCompet {
public static void sendUpdate(ArrayList<Client_Thread> client_Thread, SimpleCompet compet) {
for (Client_Thread client : client_Thread) {
client.sendNotify(compet, "sendConfig");
}
}
public static void getConfig(ArrayList<Client_Thread> client_Thread, long id_compet,
CompletableFuture<SimpleCompet> future) {
if (client_Thread.isEmpty()) return;
client_Thread.get(0).sendReq(id_compet, "getConfig",
new JsonConsumer<>(SimpleCompet.class, future::complete));
}
public static void getAllHaveAccess(ArrayList<Client_Thread> client_Thread, String userId,
CompletableFuture<HashMap<String, String>> future) {
if (client_Thread.isEmpty()) return;
client_Thread.get(0).sendReq(userId, "getAllHaveAccess",
new JsonConsumer<>(HashMap.class, future::complete));
}
public static void rmCompet(ArrayList<Client_Thread> client_Thread, long id_compet) {
for (Client_Thread client : client_Thread) {
client.sendNotify(id_compet, "rmCompet");
}
}
}

View File

@@ -0,0 +1,29 @@
package fr.titionfire.ffsaf.net2.request;
import fr.titionfire.ffsaf.net2.Client_Thread;
import fr.titionfire.ffsaf.rest.data.CompetitionData;
import java.util.ArrayList;
import java.util.HashMap;
public class SReqRegister {
public static void sendIfNeed(ArrayList<Client_Thread> client_Thread, CompetitionData.SimpleRegister simpleRegister, Long competitionId) {
HashMap<String, Object> map = new HashMap<>();
map.put("simpleRegister", simpleRegister);
map.put("competitionId", competitionId);
for (Client_Thread client : client_Thread) {
client.sendNotify(map, "sendRegister");
}
}
public static void sendRmIfNeed(ArrayList<Client_Thread> clients, Long combId, Long id) {
HashMap<String, Object> map = new HashMap<>();
map.put("combId", combId);
map.put("competitionId", id);
for (Client_Thread client : clients) {
client.sendNotify(map, "sendRmRegister");
}
}
}

View File

@@ -0,0 +1,94 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.AffiliationService;
import fr.titionfire.ffsaf.rest.data.SimpleAffiliation;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.util.List;
import java.util.function.Consumer;
@Tag(name = "Affiliation API", description = "API pour gérer les affiliations")
@Path("api/affiliation")
public class AffiliationEndpoints {
@Inject
AffiliationService service;
@Inject
SecurityCtx securityCtx;
Consumer<Long> checkPerm = Unchecked.consumer(id -> {
if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(id))
throw new DForbiddenException();
});
@GET
@Path("/current")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les affiliations pour la saison en cours", description = "Cette méthode renvoie les affiliations pour la saison en cours. Seuls les administrateurs de la fédération peuvent accéder à cette méthode.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé")
})
public Uni<List<SimpleAffiliation>> getCurrentSaisonAffiliationAdmin() {
return service.getCurrentSaisonAffiliation();
}
@GET
@Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les affiliations pour un club", description = "Cette méthode renvoie les affiliations pour un club donné. Seuls les administrateurs de la fédération et les présidents, secrétaires et responsables intranet du club peuvent accéder à cette méthode.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Club non trouvé")
})
public Uni<List<SimpleAffiliation>> getAffiliation(
@Parameter(description = "L'identifiant du club") @PathParam("id") long id) {
return Uni.createFrom().item(id).invoke(checkPerm).chain(__ -> service.getAffiliation(id));
}
@POST
@Path("{id}")
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Ajoute une affiliation pour un club", description = "Cette méthode ajoute une affiliation pour un club et une saison donné. Seuls les administrateurs de la fédération peuvent accéder à cette méthode.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Club non trouvé")
})
public Uni<SimpleAffiliation> setAffiliation(
@Parameter(description = "L'identifiant du club") @PathParam("id") long id,
@Parameter(description = "La saison à pour la quelle ajoute l'affiliation") @QueryParam("saison") int saison) {
return service.setAffiliation(id, saison);
}
@DELETE
@Path("{id}")
@RolesAllowed("federation_admin")
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Supprime une affiliation", description = "Cette méthode supprime l'affiliation {id}. Seuls les administrateurs de la fédération peuvent accéder à cette méthode.")
@APIResponses(value = {
@APIResponse(responseCode = "204", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé")
})
public Uni<?> deleteAffiliation(
@Parameter(description = "L'identifiant de l'affiliation") @PathParam("id") long id) {
return service.deleteAffiliation(id);
}
}

View File

@@ -0,0 +1,191 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.AffiliationService;
import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliation;
import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliationResume;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestSaveForm;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.utils.Utils;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import java.net.URISyntaxException;
import java.util.List;
import java.util.function.Consumer;
@Path("api/affiliation/request")
public class AffiliationRequestEndpoints {
@Inject
AffiliationService service;
@Inject
SecurityCtx securityCtx;
@ConfigProperty(name = "upload_dir")
String media;
Consumer<Long> checkPerm = Unchecked.consumer(id -> {
if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(id))
throw new DForbiddenException();
});
@GET
@Path("")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie toutes les demandes d'affiliation", description = "Cette méthode renvoie toutes les " +
"demandes d'affiliation sous forme de résumés.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé")
})
public Uni<List<SimpleReqAffiliationResume>> getAllAffRequest() {
return service.getAllReq().map(o -> o.stream().map(SimpleReqAffiliationResume::fromModel).toList());
}
@POST
@Path("")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Enregistre une nouvelle demande d'affiliation", description = "Cette méthode enregistre une " +
"nouvelle demande d'affiliation à partir des données soumises dans le formulaire. Ne nécessite pas d'authentification.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "400", description = "Données invalides"),
@APIResponse(responseCode = "403", description = "Accès refusé")
})
public Uni<String> saveAffRequest(AffiliationRequestForm form) {
return service.save(form);
}
@GET
@Path("/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie une demande d'affiliation", description = "Cette méthode renvoie une demande d'affiliation " +
"pour l'identifiant spécifié.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Demande d'affiliation non trouvée")
})
public Uni<SimpleReqAffiliation> getAffRequest(
@Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) {
return service.getRequest(id).invoke(Unchecked.consumer(o -> {
if (o.getClub() == null && !securityCtx.roleHas("federation_admin"))
throw new DForbiddenException();
})).invoke(o -> checkPerm.accept(o.getClub()));
}
@DELETE
@Path("/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Supprime une demande d'affiliation", description = "Cette méthode supprime une demande " +
"d'affiliation pour l'identifiant spécifié.")
@APIResponses(value = {
@APIResponse(responseCode = "204", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Demande d'affiliation non trouvée")
})
public Uni<?> getDelAffRequest(
@Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id, @QueryParam("reason") String reason) {
return service.getRequest(id).invoke(Unchecked.consumer(o -> {
if (o.getClub() == null && !securityCtx.roleHas("federation_admin"))
throw new DForbiddenException();
})).invoke(o -> checkPerm.accept(o.getClub()))
.chain(o -> service.deleteReqAffiliation(id, reason, securityCtx.roleHas("federation_admin")));
}
@PUT
@Path("/save")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Enregistre une demande d'affiliation en tant qu'admin", description = "Cette méthode " +
"enregistre une demande d'affiliation en tant qu'admin à partir des données soumises dans le formulaire.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "400", description = "Données invalides"),
@APIResponse(responseCode = "403", description = "Accès refusé")
})
public Uni<?> saveAdminAffRequest(AffiliationRequestSaveForm form) {
return service.saveAdmin(form);
}
@PUT
@Path("/edit")
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Modifie une demande d'affiliation", description = "Cette méthode modifie une demande " +
"d'affiliation à partir des données soumises dans le formulaire.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "400", description = "Données invalides"),
@APIResponse(responseCode = "403", description = "Accès refusé")
})
public Uni<?> saveEditAffRequest(AffiliationRequestForm form) {
return service.saveEdit(form);
}
@PUT
@Path("/apply")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Accepte une demande d'affiliation", description = "Cette méthode accepte une demande " +
"d'affiliation à partir des données soumises dans le formulaire.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "400", description = "Données invalides"),
@APIResponse(responseCode = "403", description = "Accès refusé")
})
public Uni<?> acceptAffRequest(AffiliationRequestSaveForm form) {
return service.accept(form);
}
@GET
@Path("/{id}/logo")
@RolesAllowed({"federation_admin"})
@Operation(summary = "Renvoie le logo d'une demande d'affiliation", description = "Cette méthode renvoie le logo" +
" d'une demande d'affiliation pour l'identifiant spécifié.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Logo non trouvé")
})
public Uni<Response> getLogo(
@Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) throws URISyntaxException {
return Utils.getMediaFile(id, media, "aff_request/logo", Uni.createFrom().nullItem());
}
@GET
@Path("/{id}/status")
@RolesAllowed({"federation_admin"})
@Operation(summary = "Renvoie le statut d'une demande d'affiliation", description = "Cette méthode renvoie le statut" +
" d'une demande d'affiliation pour l'identifiant spécifié.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Statut non trouvé")
})
public Uni<Response> getStatus(
@Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) throws URISyntaxException {
return Utils.getMediaFile(id, media, "aff_request/status", "affiliation_request_" + id,
Uni.createFrom().nullItem(), false);
}
}

View File

@@ -0,0 +1,43 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.rest.client.SirenService;
import fr.titionfire.ffsaf.rest.client.StateIdService;
import fr.titionfire.ffsaf.rest.data.AssoData;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@Path("api/asso")
public class AssoEndpoints {
@RestClient
StateIdService stateIdService;
@RestClient
SirenService sirenService;
@Inject
TradService trad;
@GET
@Path("state_id/{stateId}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true)
public Uni<AssoData> getAssoInfo(@PathParam("stateId") String stateId) {
return ((stateId.charAt(0) == 'W') ? stateIdService.get_rna(stateId) : sirenService.get_unite(
stateId).chain(stateIdService::getAssoDataFromUnit)).onFailure().transform(throwable -> {
if (throwable instanceof WebApplicationException exception) {
if (exception.getResponse().getStatus() == 404)
return new DNotFoundException(trad.t("service.momentanement.indisponible"));
if (exception.getResponse().getStatus() == 400)
return new DNotFoundException(trad.t("asso.introuvable"));
}
return throwable;
});
}
}

View File

@@ -11,6 +11,9 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import java.net.URI;
import java.net.URISyntaxException;
@@ -25,10 +28,16 @@ public class AuthEndpoints {
SecurityIdentity securityIdentity;
@Inject
JsonWebToken accessToken;
JsonWebToken IdToken;
@GET
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Vérifie si l'utilisateur est authentifié", description = "Cette méthode renvoie true si " +
"l'utilisateur est authentifié et false sinon.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite")
})
public Boolean auth() {
return !securityIdentity.isAnonymous();
}
@@ -37,14 +46,21 @@ public class AuthEndpoints {
@Path("/userinfo")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les informations de l'utilisateur authentifié", description = "Cette méthode renvoie les" +
" informations de l'utilisateur authentifié sous forme d'objet JSON.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Réussite"),
@APIResponse(responseCode = "401", description = "Utilisateur non authentifié")
})
public UserInfo userinfo() {
return UserInfo.makeUserInfo(accessToken, securityIdentity);
return UserInfo.makeUserInfo(IdToken, securityIdentity);
}
@GET
@Path("/login")
@Authenticated
@Produces(MediaType.TEXT_PLAIN)
@Operation(hidden = true)
public Response login() throws URISyntaxException {
return Response.temporaryRedirect(new URI(redirect)).build();
}

View File

@@ -0,0 +1,63 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.CategoryService;
import fr.titionfire.ffsaf.rest.data.CategoryData;
import fr.titionfire.ffsaf.rest.data.CategoryFullData;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Authenticated
@Path("api/poule/{system}/admin/")
public class CategoryAdminEndpoints {
@PathParam("system")
private CompetitionSystem system;
@Inject
CategoryService service;
@Inject
SecurityCtx securityCtx;
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Uni<CategoryData> getByIdAdmin(@PathParam("id") Long id) {
return service.getByIdAdmin(securityCtx, system, id);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<CategoryData>> getAllAdmin() {
return service.getAllAdmin(securityCtx, system);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Uni<CategoryData> addOrUpdate(CategoryData data) {
return service.addOrUpdate(securityCtx, system, data);
}
@POST
@Path("sync")
@Consumes(MediaType.APPLICATION_JSON)
public Uni<?> syncCategory(CategoryFullData data) {
return service.syncCategory(securityCtx, system, data);
}
@DELETE
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> delete(@PathParam("id") Long id) {
return service.delete(securityCtx, system, id);
}
}

View File

@@ -1,28 +1,354 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.domain.service.ClubService;
import fr.titionfire.ffsaf.domain.service.PDFService;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.domain.service.VirusScannerService;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.from.FullClubForm;
import fr.titionfire.ffsaf.rest.from.PartClubForm;
import fr.titionfire.ffsaf.utils.Contact;
import fr.titionfire.ffsaf.utils.PageResult;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.net.URISyntaxException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
@Tag(name = "Club", description = "Gestion des clubs")
@Path("api/club")
public class ClubEndpoints {
@Inject
ClubService clubService;
@Inject
PDFService pdfService;
@Inject
SecurityCtx securityCtx;
@Inject
VirusScannerService scannerService;
@ConfigProperty(name = "upload_dir")
String media;
@Inject
TradService trad;
Consumer<ClubModel> checkPerm = Unchecked.consumer(clubModel -> {
if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(clubModel.getId()))
throw new DForbiddenException();
});
Consumer<Long> checkPerm2 = Unchecked.consumer(id -> {
if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(id))
throw new DForbiddenException();
});
@GET
@Path("/no_detail")
@RolesAllowed("federation_admin")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie la liste de tous les clubs sans détails", description = "Renvoie la liste de tous les " +
"clubs sans les détails des membres et des affiliations")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "La liste de tous les clubs sans détails"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<List<SimpleClubModel>> getAll() {
return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList());
return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).sorted(
Comparator.comparing(SimpleClubModel::getName)).toList());
}
@GET
@Path("/contact_type")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les types de contacts pour les clubs", description = "Renvoie la liste des types de " +
"contacts possibles pour les clubs")
public Uni<HashMap<String, String>> getConcatType() {
return Uni.createFrom().item(Contact.toSite(trad));
}
@GET
@Path("/find")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Recherche des clubs en fonction de critères de recherche", description = "Recherche des clubs " +
"en fonction de critères de recherche tels que le nom, le pays, etc.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "La liste des clubs correspondant aux critères de recherche"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<PageResult<SimpleClubList>> getFindAdmin(
@Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit,
@Parameter(description = "Page à consulter") @QueryParam("page") Integer page,
@Parameter(description = "Text à rechercher") @QueryParam("search") String search,
@Parameter(description = "Pays à filter") @QueryParam("country") String country) {
if (limit == null)
limit = 50;
if (page == null || page < 1)
page = 1;
return clubService.search(limit, page - 1, search, country);
}
@GET
@Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les détails d'un club en fonction de son identifiant", description = "Renvoie les " +
"détails d'un club en fonction de son identifiant, y compris les informations sur les membres et les affiliations")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les détails du club"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le club n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<SimpleClub> getById(
@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel)
.invoke(m -> m.setContactMap(Contact.toSite(trad)));
}
@PUT
@Path("{id}")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Met à jour les informations d'un club en fonction de son identifiant", description = "Met à " +
"jour les informations d'un club en fonction de son identifiant, y compris les informations sur les membres" +
" et les affiliations")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Le club a été mis à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le club n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<String> setAdminClub(
@Parameter(description = "Identifiant de club") @PathParam("id") long id, FullClubForm input) {
return clubService.update(id, input)
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
}))
.chain(() -> Utils.uploadFile(scannerService, input.getLogo(), id, media, "ppClub"))
.chain(() -> Utils.uploadFile(scannerService, input.getStatus(), id, media, "clubStatus"));
}
@PUT
@RolesAllowed({"federation_admin"})
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Ajoute un nouveau club", description = "Ajoute un nouveau club avec les informations fournies" +
" dans le formulaire")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Le club a été ajouté avec succès"),
@APIResponse(responseCode = "400", description = "Les données envoyées sont invalides"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<Long> addAdminClub(FullClubForm input) {
return clubService.add(input)
.invoke(Unchecked.consumer(id -> {
if (id == null) throw new InternalError("Fail to create club data");
}))
.call(id -> Utils.uploadFile(scannerService, input.getLogo(), id, media, "ppClub"))
.call(id -> Utils.uploadFile(scannerService, input.getStatus(), id, media, "clubStatus"));
}
@DELETE
@Path("{id}")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Supprime un club en fonction de son identifiant", description = "Supprime un club en fonction" +
" de son identifiant, ainsi que toutes les informations associées")
@APIResponses(value = {
@APIResponse(responseCode = "204", description = "Le club a été supprimé avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le club n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<?> deleteAdminClub(
@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
return clubService.delete(id);
}
@GET
@Path("/{id}/affiliation")
@RolesAllowed({"federation_admin"})
@Operation(summary = "Renvoie l'attestation d'affiliation du club en fonction de son identifiant", description =
"Renvoie l'attestation d'affiliation du club en fonction de son identifiant")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "L'attestation d'affiliation"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le club n'existe pas ou n'a pas d'affiliation active"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<Response> getAffiliation(@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
return pdfService.getAffiliationPdf(id);
}
@GET
@Path("/me")
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les informations du club de l'utilisateur connecté", description = "Renvoie les " +
"informations du club de l'utilisateur connecté")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les informations du club de l'utilisateur connecté"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "L'utilisateur n'est pas membre d'un club"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<SimpleClub> getOfUser() {
return clubService.getOfUser(securityCtx).map(SimpleClub::fromModel)
.invoke(m -> m.setContactMap(Contact.toSite(trad)));
}
@PUT
@Path("/me")
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Met à jour les informations du club de l'utilisateur connecté", description = "Met à jour les" +
" informations du club de l'utilisateur connecté")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les informations du club de l'utilisateur connecté ont été mises à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "L'utilisateur n'est pas membre d'un club"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<String> setClubOfUser(PartClubForm form) {
return clubService.updateOfUser(securityCtx, form);
}
@GET
@Path("/me/affiliation")
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie l'attestation d'affiliation du club de l'utilisateur connecté", description =
"Renvoie l'attestation d'affiliation du club de l'utilisateur connecté")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "L'attestation d'affiliation"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le club n'a pas d'affiliation active"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<Response> getMeAffiliation() {
return pdfService.getAffiliationPdf(securityCtx.getSubject());
}
@GET
@Path("/members")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra", "club_tresorier"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Revoie tout les membres de votre club")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "List des membres"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "L'utilisateur n'est pas membre d'un club"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<List<VerySimpleMembre>> getMembers() {
return clubService.getMembers(securityCtx);
}
@GET
@Path("/renew/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true)
public Uni<RenewAffData> getRenew(@PathParam("id") long id, @QueryParam("m1") long m1_id,
@QueryParam("m2") long m2_id, @QueryParam("m3") long m3_id) {
return Uni.createFrom().item(id).invoke(checkPerm2)
.chain(__ -> clubService.getRenewData(id, List.of(m1_id, m2_id, m3_id)));
}
@GET
@Path("/desk/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie la liste des membres du bureau du club", description = "Renvoie la liste des membres " +
"du bureau du club spécifié")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "La liste des membres du bureau du club"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le club n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<List<DeskMember>> getClubDesk(
@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
return clubService.getClubDesk(checkPerm, id);
}
@GET
@Path("{clubId}/logo")
@Operation(summary = "Renvoie le logo du club", description = "Renvoie le logo du club spécifié")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Le logo du club"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le club n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<Response> getLogo(
@Parameter(description = "Identifiant long (clubId) de club") @PathParam("clubId") String clubId) {
return clubService.getByClubId(clubId).chain(Unchecked.function(clubModel -> {
try {
return Utils.getMediaFileNoDefault((clubModel != null) ? clubModel.getId() : -1, media, "ppClub",
Uni.createFrom().nullItem());
} catch (URISyntaxException e) {
throw new InternalError();
}
}));
}
@GET
@Path("{id}/status")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie le statut du club", description = "Renvoie le statut du club spécifié")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Le statut du club"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le club n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<Response> getStatus(
@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
return clubService.getById(id).onItem().invoke(checkPerm).chain(Unchecked.function(clubModel -> {
try {
return Utils.getMediaFile(clubModel.getId(), media, "clubStatus",
"statue-" + clubModel.getName(), Uni.createFrom().nullItem(), false);
} catch (URISyntaxException e) {
throw new InternalError();
}
}));
}
@GET
@Path("get_map_data")
public Uni<List<ClubMapData>> getMapData() {
return clubService.getMapData();
}
}

View File

@@ -1,136 +0,0 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.MembreService;
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.utils.Pair;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jodd.net.MimeTypes;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
@Path("api/member")
public class CombEndpoints {
@Inject
MembreService membreService;
@ConfigProperty(name = "upload_dir")
String media;
@GET
@Path("/all")
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<SimpleMembre>> getAll() {
return membreService.getAll().map(membreModels -> membreModels.stream().map(SimpleMembre::fromModel).toList());
}
@GET
@Path("{id}")
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
public Uni<SimpleMembre> getById(@PathParam("id") long id) {
return membreService.getById(id).map(SimpleMembre::fromModel);
}
@POST
@Path("{id}")
@RolesAllowed("federation_admin")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Uni<String> setAdminMembre(@PathParam("id") long id, FullMemberForm input) {
Future<String> future = CompletableFuture.supplyAsync(() -> {
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input.getPhoto_data()))) {
String mimeType = URLConnection.guessContentTypeFromStream(is);
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
if (detectedExtensions.length == 0)
throw new IOException("Fail to detect file extension for MIME type " + mimeType);
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
File[] files = new File(media, "ppMembre").listFiles(filter);
if (files != null) {
for (File file : files) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
String extension = "." + detectedExtensions[0];
Files.write(new File(media, "ppMembre/" + input.getId() + extension).toPath(), input.getPhoto_data());
return "OK";
} catch (IOException e) {
return e.getMessage();
}
});
if (input.getPhoto_data().length > 0) {
return membreService.update(id, input)
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
}))
.chain(() -> Uni.createFrom().future(future))
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
}));
} else {
return membreService.update(id, input)
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
}));
}
}
@GET
@Path("{id}/photo")
@RolesAllowed("federation_admin")
public Uni<Response> getPhoto(@PathParam("id") long id) throws URISyntaxException {
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
File[] files = new File(media, "ppMembre").listFiles(filter);
if (files != null && files.length > 0) {
File file = files[0];
try {
byte[] data = Files.readAllBytes(file.toPath());
return new Pair<>(file, data);
} catch (IOException ignored) {
}
}
return null;
});
URI uri = new URI("https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava2.webp");
return Uni.createFrom().future(future)
.map(filePair -> {
if (filePair == null)
return Response.temporaryRedirect(uri).build();
String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName());
Response.ResponseBuilder resp = Response.ok(filePair.getValue());
resp.type(MediaType.APPLICATION_OCTET_STREAM);
resp.header(HttpHeaders.CONTENT_LENGTH, filePair.getValue().length);
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; ");
return resp.build();
});
}
}

View File

@@ -0,0 +1,58 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.CompetitionService;
import fr.titionfire.ffsaf.rest.data.CompetitionData;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("api/competition/admin")
public class CompetitionAdminEndpoints {
@Inject
CompetitionService service;
@Inject
SecurityCtx securityCtx;
@GET
@Path("{id}")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<CompetitionData> getByIdAdmin(@PathParam("id") Long id) {
return service.getByIdAdmin(securityCtx, id);
}
@GET
@Path("all")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<CompetitionData>> getAllAdmin() {
return service.getAllAdmin(securityCtx);
}
@GET
@Path("all/{system}")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<CompetitionData>> getAllSystemAdmin(@PathParam("system") CompetitionSystem system) {
return service.getAllSystemAdmin(securityCtx, system);
}
@GET
@Path("all/{system}/table")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<CompetitionData>> getAllSystemTable(@PathParam("system") CompetitionSystem system) {
return service.getAllSystemTable(securityCtx, system);
}
}

View File

@@ -0,0 +1,135 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.CompetitionService;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import java.util.List;
@Path("api/competition/")
public class CompetitionEndpoints {
@Inject
CompetitionService service;
@Inject
SecurityCtx securityCtx;
@GET
@Path("{id}")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<CompetitionData> getById(@PathParam("id") Long id, @QueryParam("light") boolean light) {
if (light)
return service.getById(securityCtx, id);
else
return service.getByIdAdmin(securityCtx, id);
}
@GET
@Path("{id}/register/{source}")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<SimpleRegisterComb>> getRegister(@PathParam("id") Long id, @PathParam("source") String source) {
return service.getRegister(securityCtx, id, source);
}
@POST
@Path("{id}/register/{source}")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true)
public Uni<SimpleRegisterComb> addRegisterComb(@PathParam("id") Long id, @PathParam("source") String source,
RegisterRequestData data) {
return service.addRegisterComb(securityCtx, id, data, source);
}
@POST
@Path("{id}/registers/{source}")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true)
public Uni<List<SimpleRegisterComb>> addRegistersComb(@PathParam("id") Long id, @PathParam("source") String source,
List<RegisterRequestData> data) {
return service.addRegistersComb(securityCtx, id, data, source);
}
@DELETE
@Path("{id}/register/{comb_id}/{source}")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true)
public Uni<Void> removeRegisterComb(@PathParam("id") Long id, @PathParam("comb_id") Long combId,
@PathParam("source") String source, @QueryParam("ban") boolean ban) {
return service.removeRegisterComb(securityCtx, id, combId, source, ban);
}
@GET
@Path("{id}/safcaData")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<SimpleCompetData> getSafcaData(@PathParam("id") Long id) {
return service.getSafcaData(securityCtx, id);
}
@GET
@Path("{id}/internalData")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<SimpleCompetData> getInternalData(@PathParam("id") Long id) {
return service.getInternalData(securityCtx, id);
}
@GET
@Path("{id}/categories")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<PresetData>> getPresetsForCompetition(@PathParam("id") Long id) {
return service.getPresetsForCompetition(securityCtx, id);
}
@GET
@Path("all")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<CompetitionData>> getAll() {
return service.getAll(securityCtx);
}
@POST
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<CompetitionData> addOrUpdate(CompetitionData data) {
return service.addOrUpdate(securityCtx, data);
}
@POST
@Path("/safcaData")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> setSafcaData(SimpleCompetData data) {
return service.setSafcaData(securityCtx, data);
}
@POST
@Path("/internalData")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> setInternalData(SimpleCompetData data) {
return service.setInternalData(securityCtx, data);
}
@DELETE
@Path("{id}")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> delete(@PathParam("id") Long id) {
return service.delete(securityCtx, id);
}
}

View File

@@ -1,48 +1,110 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.KeycloakService;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.from.MemberPermForm;
import fr.titionfire.ffsaf.utils.Pair;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.core.Vertx;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.keycloak.representations.idm.GroupRepresentation;
import java.util.ArrayList;
import java.util.List;
@Tag(name = "Compte", description = "Gestion des comptes utilisateurs")
@Path("api/compte")
public class CompteEndpoints {
@Inject
KeycloakService service;
@Inject
SecurityCtx securityCtx;
@Inject
Vertx vertx;
@GET
@Path("{id}")
@RolesAllowed("federation_admin")
public Uni<?> getCompte(@PathParam("id") String id) {
return service.fetchCompte(id);
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie les informations d'un compte utilisateur", description = "Renvoie les informations d'un" +
" compte utilisateur en fonction de son identifiant long (UUID)")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les informations du compte utilisateur"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le compte utilisateur n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<KeycloakService.UserCompteState> getCompte(@PathParam("id") String id) {
return service.fetchCompte(id).call(pair -> vertx.getOrCreateContext().executeBlocking(() -> {
if (!securityCtx.roleHas("federation_admin") && pair.getKey().groups().stream()
.map(GroupRepresentation::getPath)
.noneMatch(s -> s.startsWith("/club/") && securityCtx.contains(s)))
throw new DForbiddenException();
return pair;
})).map(Pair::getValue);
}
@PUT
@Path("{id}/init")
@RolesAllowed("federation_admin")
@Operation(summary = "Initialise un compte utilisateur", description = "Initialise un compte utilisateur en fonction" +
" de son identifiant")
@APIResponses(value = {
@APIResponse(responseCode = "204", description = "Le compte utilisateur a été initialisé avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le membre n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<?> initCompte(@PathParam("id") long id) {
return service.initCompte(id);
}
@PUT
@Path("{id}/setUUID/{nid}")
@RolesAllowed("federation_admin")
@Operation(hidden = true)
public Uni<?> initCompte(@PathParam("id") long id, @PathParam("nid") String nid) {
return service.setId(id, nid);
}
@GET
@Path("{id}/roles")
@RolesAllowed("federation_admin")
public Uni<?> getRole(@PathParam("id") String id) {
@Operation(summary = "Renvoie les rôles d'un compte utilisateur", description = "Renvoie les rôles d'un compte" +
" utilisateur en fonction de son identifiant")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les rôles du compte utilisateur"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le compte utilisateur n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<List<String>> getRole(@PathParam("id") String id) {
return service.fetchRole(id);
}
@PUT
@Path("{id}/roles")
@RolesAllowed("federation_admin")
@Operation(summary = "Met à jour les rôles d'un compte utilisateur", description = "Met à jour les rôles d'un compte" +
" utilisateur en fonction de son identifiant et des rôles fournis dans le formulaire")
@APIResponses(value = {
@APIResponse(responseCode = "204", description = "Les rôles du compte utilisateur ont été mis à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le compte utilisateur n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<?> updateRole(@PathParam("id") String id, MemberPermForm form) {
List<String> toAdd = new ArrayList<>();
List<String> toRemove = new ArrayList<>();
@@ -53,8 +115,8 @@ public class CompteEndpoints {
else toRemove.add("safca_super_admin");
if (form.isSafca_user()) toAdd.add("safca_user");
else toRemove.add("safca_user");
if (form.isSafca_create_compet()) toAdd.add("safca_create_compet");
else toRemove.add("safca_create_compet");
if (form.isCreate_compet()) toAdd.add("create_compet");
else toRemove.add("create_compet");
return service.updateRole(id, toAdd, toRemove);
}

View File

@@ -0,0 +1,34 @@
package fr.titionfire.ffsaf.rest;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import java.util.HashMap;
import java.util.Locale;
@Path("api/countries")
public class CountriesEndpoints {
@GET
@Path("/{lang}/{code}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true)
public Uni<HashMap<String, String>> getCountries(@PathParam("lang") String lang, @PathParam("code") String code) {
Locale locale = new Locale(lang, code);
return Uni.createFrom().item(new HashMap<String, String>())
.invoke(map -> {
String[] locales = Locale.getISOCountries();
for (String countryCode : locales) {
if (countryCode.equals("AN"))
continue;
Locale obj = new Locale("", countryCode);
map.put(countryCode, obj.getDisplayName(locale));
}
});
}
}

View File

@@ -0,0 +1,92 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.ResultService;
import fr.titionfire.ffsaf.domain.service.UpdateService;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.HashMap;
import java.util.List;
@Path("api/public/result/{id}")
public class ExternalResultEndpoints {
@Inject
ResultService resultService;
@Inject
UpdateService updateService;
@PathParam("id")
private String id;
@GET
@Path("/poule/list")
@Produces(MediaType.APPLICATION_JSON)
public Uni<HashMap<String, Long>> list() {
return resultService.getCategoryList(id);
}
@GET
@Path("/poule/data")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> getArray(@QueryParam("poule") long poule, @DefaultValue("-1") @QueryParam("rf") long rf) {
if (poule == 0)
return Uni.createFrom().voidItem();
if (updateService.needUpdate(poule, rf)) {
return resultService.getCategory(id, poule);
} else {
return Uni.createFrom().voidItem();
}
}
@GET
@Path("/comb/list")
@Produces(MediaType.APPLICATION_JSON)
public Uni<HashMap<String, String>> combList() {
return resultService.getCombList(id);
}
@GET
@Path("/comb/data")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> getArray(@QueryParam("comb") String comb) {
if (comb.equals("0"))
return Uni.createFrom().item("");
return resultService.getCombArrayPublic(id, comb);
}
@GET
@Path("/comb/get_all")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> getAll() {
return resultService.getAllCombArrayPublic(id);
}
@GET
@Path("/club/list")
@Produces(MediaType.APPLICATION_JSON)
public Uni<HashMap<String, Long>> clubList() {
return resultService.getClubList(id);
}
@GET
@Path("/club/classement")
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<ResultService.ClubClassement>> clubClassement() {
return resultService.getAllClubArray(id);
}
@GET
@Path("/club/data")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> getClubArray(@QueryParam("club") long club) {
if (club == 0)
return Uni.createFrom().item("");
return resultService.getClubArray2(id, club, null);
}
}

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