Compare commits

...

290 Commits

Author SHA1 Message Date
6e1ee839ba
Merge pull request 'modernize-oauth' (!76) from modernize-oauth into master
All checks were successful
rvr-nextgen/pipeline/tag This commit looks good
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #76
2024-11-26 21:32:41 +01:00
cac57d7f71
decease session expiration time
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2024-11-26 21:11:27 +01:00
cde14ee779
modernize oauth token handling 2024-11-26 21:11:27 +01:00
dfcdd8dca7
update yarn.lock 2024-11-26 21:07:04 +01:00
a5286bf62f
add .well-known/openid-configuration to the root as well 2024-11-26 21:07:04 +01:00
ec4d3806ce
Merge pull request 'do not recreate docker runner group and user' (!75) from do-no-recreate-group-and-user into master
All checks were successful
rvr-nextgen/pipeline/tag This commit looks good
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #75
2024-03-11 00:15:07 +01:00
0c2334502d
do not recreate docker runner group and user
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2024-03-10 23:53:11 +01:00
6341072b0b
Merge pull request 'fix regex for tagging' (!74) from bugfix/fix-regex into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #74
2023-10-01 00:15:15 +02:00
1dbd813bef
fix regex for tagging
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-10-01 00:13:27 +02:00
14d83d24b4
Merge pull request 'push fixed version images' (!73) from feature/auto-release-fixed-tags into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #73
2023-10-01 00:07:52 +02:00
ba02b6d8cb
push fixed version images
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-10-01 00:06:38 +02:00
9b8fcaad9a
Merge pull request 'error reporting should always be E_ALL' (!72) from bugfix/fix-error-reporting into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #72
2023-09-30 23:37:24 +02:00
6bd6ede442
error reporting should always be E_ALL
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-30 23:18:42 +02:00
65dac4640a
Merge pull request 'define forwarded_scheme before use' (!71) from bugfix/pass-scheme-to-fastcgi into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #71
2023-09-28 15:00:21 +02:00
7bd12050f6
define forwarded_scheme before use
Some checks are pending
rvr-nextgen/pipeline/pr-master Build queued...
2023-09-28 14:59:55 +02:00
5041258de0
Merge pull request 'pass scheme to fastcgi' (!70) from bugfix/pass-scheme-to-fastcgi into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #70
2023-09-28 14:55:37 +02:00
47928d2d2b
pass scheme to fastcgi
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 14:43:30 +02:00
55cf2afde3
Merge pull request 'feature/update-to-ubuntu-2204' (!69) from feature/update-to-ubuntu-2204 into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #69
2023-09-28 14:42:43 +02:00
ed2b1c23ae
update soko-web
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 14:30:59 +02:00
6ef45b8b6a
generate composer.lock 2023-09-28 14:30:58 +02:00
b4c359e81d
update phpunit 2023-09-28 14:30:58 +02:00
495a2fe910
update ubuntu to 22.04 and php to 8.1 2023-09-28 14:30:58 +02:00
37ba0ec172
Merge pull request 'feature/create-working-docker-images' (!68) from feature/create-working-docker-images into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #68
2023-09-28 13:33:02 +02:00
9ffde6bccb
update readme
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 13:31:26 +02:00
0ce1c4f28a
update soko-web 2023-09-28 13:21:54 +02:00
0d1e4a3d1c
update docker-compose 2023-09-28 13:21:54 +02:00
cbf62d1c4a
add docker release stage to pipeline 2023-09-28 13:21:54 +02:00
5eeac18b4c
use the new dockerfile in pipeline 2023-09-28 13:21:54 +02:00
c4dce94f5e
delete deprecated dockerfiles 2023-09-28 13:21:54 +02:00
147b7690ac
add new dockerfile with dev and release stages 2023-09-28 13:21:54 +02:00
bdfa46b838
add entry point for dev docker 2023-09-28 13:21:54 +02:00
27fe883f59
add entry point for release docker 2023-09-28 13:21:54 +02:00
1718dfca9e
install base database in MigrateDatabaseCommand 2023-09-28 13:21:54 +02:00
6870d08c21
remove deprecated scripts 2023-09-28 13:21:54 +02:00
eba674bfbc
add cron for db:maintain 2023-09-28 13:21:54 +02:00
832170b1e1
add release generator script 2023-09-28 13:21:54 +02:00
8dfea0993b
add nodejs installer script 2023-09-28 13:21:54 +02:00
6146641eed
Merge pull request 'fix selectUpcomingAndRecent when used with other selects' (!67) from bugfix/upcoming-events-are-multiplicated into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #67
2023-08-06 22:09:33 +02:00
7cb406cc49
fix selectUpcomingAndRecent when used with other selects
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-08-06 22:06:42 +02:00
2d2cf9c6f0
Merge pull request 'make short new transaction url even shorter' (!66) from feature/change-short-new-transaction-url into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #66
2023-07-24 21:43:39 +02:00
ca069d0795
make short new transaction url even shorter
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 21:42:27 +02:00
17d9437a50
Merge pull request 'fix select to avoid getting all events as upcoming/recent' (!65) from bugfix/fix-select-for-upcoming-and-recent-events into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #65
2023-07-24 02:12:15 +02:00
aed4548c7b
fix select to avoid getting all events as upcoming/recent
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 02:11:27 +02:00
0d6d152b82
Merge pull request 'create endpoints to get current event and current event new transaction' (!64) from feature/create-endpoint-for-currect-event-redirection into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #64
2023-07-24 02:08:11 +02:00
7beec510a1
create endpoints to get current event and current event new transaction
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 02:00:59 +02:00
22b849a1c8
Merge pull request 'select current user as payer by default for new transaction' (!63) from feature/default-payer-is-current-user into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #63
2023-07-24 01:31:22 +02:00
492aa9fcef
select current user as payer by default for new transaction
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 01:29:53 +02:00
b6da70e015
Merge pull request 'fix transaction edit header' (!62) from bugfix/fix-transaction-edit-header into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #62
2023-07-12 00:40:06 +02:00
afead7fd61
Merge pull request 'show balance for events' (!61) from feature/show-balance-for-events into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #61
2023-07-12 00:38:17 +02:00
69acbf708b
fix transaction edit header
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-12 00:32:55 +02:00
08ebfae030
show balance for events
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-12 00:26:59 +02:00
a597839379
Merge pull request 'feature/RVRNEXT-45-show-recent-events-as-well' (!60) from feature/RVRNEXT-45-show-recent-events-as-well into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #60
2023-07-08 14:44:35 +02:00
e0fe6a8622
RVRNEXT-45 update soko-web to 0.13.1
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-08 14:42:09 +02:00
5ebce653dc
RVRNEXT-45 show recent events as well with upcoming events 2023-07-08 13:52:24 +02:00
61eb2404a2
Merge pull request 'feature/RVRNEXT-43-split-transactions' (!59) from feature/RVRNEXT-43-split-transactions into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #59
2023-06-17 14:59:09 +02:00
e199dfccc7
RVRNEXT-43 update soko-web
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-06-17 14:35:00 +02:00
1d0a2b09d5
RVRNEXT-43 adapt balance calculations to multiple payees 2023-06-17 13:47:05 +02:00
1886257b63
RVRNEXT-43 adapt views to multiple payees 2023-06-17 13:46:43 +02:00
eda1415e86
RVRNEXT-43 implement multiple payees in transaction controller 2023-06-17 13:46:13 +02:00
4e9c7f8c3c
RVRNEXT-43 extend community member repository with count 2023-06-17 13:17:29 +02:00
eebf4aff00
RVRNEXT-43 add multi relation for transaction 2023-06-17 13:16:53 +02:00
6dae33507f
RVRNEXT-43 add repository for transaction payees 2023-06-17 13:15:54 +02:00
96d5aa2219
RVRNEXT-43 add model for transaction payee 2023-06-17 13:14:43 +02:00
6c326b89e2
RVRNEXT-43 add migration for transaction payees 2023-06-17 13:14:33 +02:00
6c821d41b4
Merge pull request 'feature/RVRNEXT-39-delete-communities' (!58) from feature/RVRNEXT-39-delete-communities into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #58
2023-06-16 21:39:45 +02:00
ade02f4736
RVRNEXT-39 add endpoint for community delete
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-06-16 21:38:27 +02:00
bb5b510315
RVRNEXT-39 add delete button for communities 2023-06-16 21:38:27 +02:00
1267cb3d95
RVRNEXT-39 implement community delete 2023-06-16 21:38:26 +02:00
c7641b85e7
RVRNEXT-39 make main currency nullable 2023-06-16 21:28:42 +02:00
ee4b8e9d65
Merge pull request 'bugfix/RVRNEXT-31-allow-special-characters-for-local-assets' (!57) from bugfix/RVRNEXT-31-allow-special-characters-for-local-assets into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #57
2023-05-28 22:31:21 +02:00
58d4f982e9
RVRNEXT-31 remove hack for assets of tom-select
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 22:30:00 +02:00
ad06cfaa58
RVRNEXT-31 update soko-web 2023-05-28 22:30:00 +02:00
aa2e0e9b4a
Merge pull request 'feature/RVRNEXT-22-add-pagination-template' (!56) from feature/RVRNEXT-22-add-pagination-template into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #56
2023-05-28 20:46:22 +02:00
d1c926541a
RVRNEXT-22 use the new repository methods in controllers
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 20:34:07 +02:00
9929e00a3f
RVRNEXT-22 use Select::paginate in repositories 2023-05-28 20:34:07 +02:00
bf415a2a46
RVRNEXT-22 use pagination template 2023-05-28 20:34:00 +02:00
c0bfa5b334
RVRNEXT-22 add pagination template 2023-05-28 20:33:59 +02:00
e122248a97
Merge pull request 'fix sql statement for table transactions' (!55) from bugfix/fix-buggy-migration into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #55
2023-05-28 17:03:11 +02:00
d2bb1d4936
fix sql statement for table transactions
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 17:01:19 +02:00
b8a901b041
Merge pull request 'feature/RVRNEXT-11-events' (!54) from feature/RVRNEXT-11-events into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #54
2023-05-28 16:52:48 +02:00
ede5c24a5d
RVRNEXT-11 fix archive in pipeline
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 16:48:38 +02:00
0ae7040804
RVRNEXT-11 update soko-web to 0.12.1 2023-05-28 16:48:38 +02:00
f370ccba88
RVRNEXT-11 add upcoming events for user 2023-05-28 16:48:38 +02:00
a128168e7f
RVRNEXT-11 fix select in community members form 2023-05-28 16:48:38 +02:00
3e9c25f6fe
RVRNEXT-11 style improvements 2023-05-28 16:48:38 +02:00
352311a82e
RVRNEXT-11 add custom title for main layout 2023-05-28 16:48:38 +02:00
c97d80a724
RVRNEXT-11 add upcoming events for community 2023-05-28 16:48:38 +02:00
532dbfd8d6
RVRNEXT-11 add event for transactions 2023-05-28 16:48:37 +02:00
49f3a371e5
RVRNEXT-11 add views for events 2023-05-28 16:48:37 +02:00
2bcaf13ee3
RVRNEXT-11 add controller for events 2023-05-28 16:48:37 +02:00
2fc5812eba
RVRNEXT-11 add new endpoints for events 2023-05-28 11:29:36 +02:00
63ca6e7557
RVRNEXT-11 add model and repository for events 2023-05-28 11:29:36 +02:00
224cdc0eda
RVRNEXT-11 add migrations for events 2023-05-28 11:29:35 +02:00
42da9eb778
Merge pull request 'bugfix/fixes-for-password-reset' (!53) from bugfix/fixes-for-password-reset into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #53
2023-05-26 16:08:50 +02:00
d1dee27191
reset grecaptcha in case of error
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-08 18:48:43 +02:00
726b1818d0
fix error message for password reset request 2023-05-08 18:48:12 +02:00
75f0ae7668
Merge pull request 'feature/RVRNEXT-24-use-slugs-for-communities' (!52) from feature/RVRNEXT-24-use-slugs-for-communities into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #52
2023-05-07 01:57:57 +02:00
b43ed72040
RVRNEXT-24 replace id-based search to slug-based for communities
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-07 01:54:10 +02:00
5dc4350b20
RVRNEXT-24 modify model Community to have slug 2023-05-07 01:54:10 +02:00
829588150a
RVRNEXT-24 add migration for slug for communities 2023-05-07 01:54:10 +02:00
54d901e45e
RVRNEXT-24 update soko-web 2023-05-07 01:54:10 +02:00
5e1692efdb
Merge pull request 'feature/RVRNEXT-38-use-font-awesome' (!51) from feature/RVRNEXT-38-use-font-awesome into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #51
2023-05-06 21:52:26 +02:00
32733668ff
RVRNEXT-38 do not reference authenticateWithGoogleButton whet it does not exist
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 21:50:46 +02:00
3034217ff8
RVRNEXT-38 fix template for password reset
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 21:43:37 +02:00
c664b0a4bf
RVRNEXT-38 make it possible to show fa-icons in confirmation modals 2023-05-06 21:43:23 +02:00
f397354c1e
RVRNEXT-38 replace raw character to fa-icon in transactions 2023-05-06 21:43:01 +02:00
e3992d9d55
RVRNEXT-38 add fa-icons to buttons 2023-05-06 21:42:43 +02:00
0f7c9aa02f
RVRNEXT-38 replace svgs to fa-icons in normal layous 2023-05-06 21:41:46 +02:00
b531589c52
RVRNEXT-38 load font-awesome css 2023-05-06 21:40:56 +02:00
dc824a0f70
RVRNEXT-38 install font-awesome 2023-05-06 21:40:35 +02:00
1a06326589
Merge pull request 'feature/RVRNEXT-27-add-connect-with-google' (!50) from feature/RVRNEXT-27-add-connect-with-google into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #50
2023-05-06 19:55:16 +02:00
9780aa62cb
RVRNEXT-27 delete unused mail templates
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 19:49:20 +02:00
cc3cfc15c8
RVRNEXT-27 fix page header in google_login_error 2023-05-06 19:49:05 +02:00
a877484d56
RVRNEXT-27 add link to google connect/disconnect to account page 2023-05-06 19:48:54 +02:00
da5c2121ba
RVRNEXT-27 add views for google connect/disconnect 2023-05-06 19:48:03 +02:00
73c7d8434e
RVRNEXT-27 add logic for google connect/disconnect 2023-05-06 19:46:34 +02:00
3f7817fd1f
RVRNEXT-27 add new endpoints for google connect/disconnect 2023-05-06 19:46:09 +02:00
361671a362
Merge pull request 'use the DateTime object when comparing dates in ExhcnageRateCalculator' (!49) from bugfix/fix-current-exchange-rate-calculation into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #49
2023-05-06 13:44:09 +02:00
b2babcf30f
use the DateTime object when comparing dates in ExhcnageRateCalculator
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 13:41:17 +02:00
ea579614b3
Merge pull request 'fix margin of delete button on transaction_edit' (!48) from bugfix/layout-fix into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #48
2023-05-06 13:21:33 +02:00
d8786e74d5
fix margin of delete button on transaction_edit
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 13:13:15 +02:00
9e448e62c6
Merge pull request 'feature/RVRNEXT-34-use-text-labels-instead-of-placeholders-in-forms' (!47) from feature/RVRNEXT-34-use-text-labels-instead-of-placeholders-in-forms into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #47
2023-05-02 19:54:08 +02:00
7f1177cc55
RVRNEXT-34 swap delete and save in transaction_edit
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 19:53:06 +02:00
a941f5a1cd
RVRNEXT-34 remove placeholders, use labels instead 2023-05-02 19:51:15 +02:00
ba5dfdbe4d
RVRNEXT-34 add style class for form label 2023-05-02 19:49:43 +02:00
5557f00e57
Merge pull request 'RVRNEXT-36 add autocomplete values for username and password fields' (!46) from feature/RVRNEXT-36-add-autocompletes-for-usernames-and-passwords into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #46
2023-05-02 13:13:25 +02:00
baddc17b33
RVRNEXT-36 add autocomplete values for username and password fields
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 13:12:19 +02:00
8d495c9f37
Merge pull request 'feature/RVRNEXT-33-no-permanent-session' (!45) from feature/RVRNEXT-33-no-permanent-session into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #45
2023-05-02 13:03:32 +02:00
cc8f1e1ace
RVRNEXT-33 session should be valid for a session
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 12:59:45 +02:00
97e04d785c
RVRNEXT-33 adapt to new soko-web interfaces 2023-05-02 12:59:45 +02:00
0c102da1d7
RVRNEXT-33 update soko-web to 0.10 2023-05-02 12:58:18 +02:00
7f882f2e71
Merge pull request 'RVRNEXT-28 add further checks before manupulating db' (!44) from feature/RVRNEXT-28-check-foreign-key-constraints-before-db into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #44
2023-05-02 02:58:35 +02:00
cb76cbb9b1
RVRNEXT-28 add further checks before manupulating db
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 02:56:16 +02:00
1f32387f63
Merge pull request 'RVRNEXT-30 check if member already has associated transaction' (!43) from bugfix/RVRNEXT-29-prevent-deleting-member-who-has-transaction into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #43
2023-05-02 02:51:02 +02:00
be93c75fe7
RVRNEXT-30 check if member already has associated transaction
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 02:40:27 +02:00
38d18873b8
Merge pull request 'feature/RVRNEXT-30-replace-choices-to-tomselect' (!41) from feature/RVRNEXT-30-replace-choices-to-tomselect into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #41
2023-05-02 02:24:40 +02:00
5053d37388
RVRNEXT-30 initialize TomSelect in community_members.js
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 02:22:57 +02:00
c00be9a49d
RVRNEXT-30 remove unnecessary css selector 2023-05-02 02:22:35 +02:00
881280fa3e
RVRNEXT-30 use tom-select assets in community_member view 2023-05-02 02:22:10 +02:00
40eb8b5335
RVRNEXT-30 fix submit button name 2023-05-02 02:21:49 +02:00
789e9e199f
RVRNEXT-30 install tom-select instead of choices.js 2023-05-02 02:20:40 +02:00
be521a42ee
Merge pull request 'RVRNEXT-26 show confirmation modal before deleting something' (!40) from feature/RVRNEXT-26-confirmation-before-delete into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #40
2023-05-02 01:49:13 +02:00
65424c2944
RVRNEXT-26 show confirmation modal before deleting something
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 01:03:39 +02:00
490e48a87a
Merge pull request 'rename routes' (!39) from feature/rename-routes into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #39
2023-05-02 00:06:11 +02:00
b958a2f5b3
rename routes
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 00:02:38 +02:00
2da0ccb09b
Merge pull request 'feature/layout-improvements' (!38) from feature/layout-improvements into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #38
2023-05-01 23:42:30 +02:00
584fcaba39
make community layout clearer
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-01 23:37:30 +02:00
b5dc96f98b
fix stupid variable names 2023-05-01 23:02:57 +02:00
4dbbb1321c
Merge pull request 'feature/RVRNEXT-7-calculate-balance-for-community-members' (!37) from feature/RVRNEXT-7-calculate-balance-for-community-members into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #37
2023-05-01 22:26:03 +02:00
0a7cfdbb8e
RVRNEXT-7 show balances on community home page
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-01 22:01:11 +02:00
751c1895ca
RVRNEXT-7 calculate balances in CommunityController 2023-05-01 22:00:32 +02:00
3fd43b6e41
RVRNEXT-7 add BalanceCalculator class 2023-05-01 22:00:13 +02:00
ae3554c365
RVRNEXT-7 layout fine-tuning 2023-05-01 21:59:55 +02:00
5526a55145
RVRNEXT-7 add link for transactions 2023-05-01 19:32:35 +02:00
3e6514a2e5
Merge pull request 'feature/RVRNEXT-6-transactions' (!36) from feature/RVRNEXT-6-transactions into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #36
2023-05-01 19:28:27 +02:00
8566641459
RVRNEXT-6 add new routes for transactions
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-01 19:24:24 +02:00
14f7a5b9b5
RVRNEXT-6 add views for transactions 2023-05-01 19:23:48 +02:00
6fb951a01e
RVRNEXT-6 add transactioncontroller 2023-05-01 19:23:31 +02:00
c7c2df89c5
RVRNEXT-6 add ExchangeRateCalculator 2023-05-01 19:23:19 +02:00
67114211ec
RVRNEXT-6 add model and repository for transactions 2023-05-01 19:19:39 +02:00
f2d99c4f0f
RVRNEXT-6 add migration for transaction db structure 2023-05-01 19:19:10 +02:00
2ed01df331
RVRNEXT-6 layout fine-tuning 2023-05-01 19:18:48 +02:00
f89bed25da
RVRNEXT-6 update soko-web 2023-05-01 19:15:45 +02:00
d707c20ab6
Merge pull request 'feature/update-soko-web-to-0.7' (!35) from feature/update-soko-web-to-0.7 into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #35
2023-04-30 21:07:16 +02:00
49d72f1d48
adapt $withRelations usage to soko-web 0.7
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-30 20:25:21 +02:00
eabf17cb77
update soko-web to 0.7 2023-04-30 20:24:10 +02:00
7b45069904
Merge pull request 'feature/RVRNEXT-5-main-currency-of-community-should-be-a-general-currency' (!34) from feature/RVRNEXT-5-main-currency-of-community-should-be-a-general-currency into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #34
2023-04-28 21:44:18 +02:00
49df206ddc
RVRNEXT-5 move saveCommunity under getCommunityEdit
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-28 21:20:08 +02:00
e0ea18a6d0
RVRNEXT-5 migration for main_currency_id
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-28 21:06:36 +02:00
20a850f011
RVRNEXT-5 make community main currency a general currency 2023-04-28 21:06:18 +02:00
157f530ad5
RVRNEXT-5 add withRelations to CommunityMemberRepository getters 2023-04-28 20:59:14 +02:00
0b261b6272
RVRNEXT-5 fix audit logger for non-web access 2023-04-28 20:58:19 +02:00
882b326aca
Merge pull request 'merge new and edit methods in CommunityController' (!33) from feature/unify-saves-in-communitycontroller into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #33
2023-04-26 09:46:09 +02:00
49fe85dcde
merge new and edit methods in CommunityController
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-26 09:43:57 +02:00
a021aff92c
Merge pull request 'feature/RVRNEXT-5-handling-currency-exchange-rates' (!32) from feature/RVRNEXT-5-handling-currency-exchange-rates into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #32
2023-04-25 19:32:11 +02:00
6abe93e3b5
RVRNEXT-5 fine-tune styles
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-25 19:28:42 +02:00
7e84e6b4ba
RVRNEXT-5 add/update layout for currency exchange rates 2023-04-25 19:28:42 +02:00
76ed977375
RVRNEXT-5 add logic for handling currency exchange rates 2023-04-25 19:28:42 +02:00
fea403fe5a
RVRNEXT-5 add new routes for currency exchange rates 2023-04-25 19:28:42 +02:00
1d96fd0cb9
RVRNEXT-5 add model and repository for currency exchange rate 2023-04-25 19:28:42 +02:00
7437e19de5
RVRNEXT-5 add migration for currency exchange rates 2023-04-23 20:50:45 +02:00
47567c9b75
RVRNEXT-5 add links to community main page 2023-04-23 20:46:11 +02:00
ef16e70235
Merge pull request 'feature/RVRNEXT-5-handling-currencies' (!31) from feature/RVRNEXT-5-handling-currencies into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #31
2023-04-23 20:21:31 +02:00
91d064947e
RVRNEXT-5 formError should be searched in the whole document
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-23 20:20:10 +02:00
9a6e95673e
RVRNEXT-5 add formError for multi for layouts 2023-04-23 20:20:10 +02:00
d24c3bb1cb
RVRNEXT-5 move private methods to the end of CommunityController 2023-04-23 20:20:10 +02:00
2ad637e55f
RVRNEXT-5 make prepare-commit-msg accept fixup commits 2023-04-23 20:20:10 +02:00
ac09342c9d
RVRNEXT-5 fix layouts for communities 2023-04-23 20:20:10 +02:00
3a1054e1df
RVRNEXT-5 adjust table style 2023-04-23 20:20:10 +02:00
0f02c4b5be
RVRNEXT-5 update CommunityController with currency operations 2023-04-23 20:20:10 +02:00
be0a15d02d
RVRNEXT-5 extend views with currency edit 2023-04-23 20:09:49 +02:00
12065b0525
RVRNEXT-5 add model and repository for currencies 2023-04-23 20:09:44 +02:00
0fc21cc461
RVRNEXT-5 add migration for currencies 2023-04-23 20:09:44 +02:00
7d63de4403
RVRNEXT-5 add new endpoints for currency handling 2023-04-23 17:36:03 +02:00
c520c256ee
Merge pull request 'solve community member edit with forms' (!30) from feature/solve-community-member-edit-with-forms into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #30
2023-04-23 15:59:32 +02:00
2f9b97d1a4
solve community member edit with forms
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-23 15:55:31 +02:00
ad18c7cb25
Merge pull request 'use IRouteCollection in app container' (!29) from bugfix/fix-app-container into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #29
2023-04-20 00:34:26 +02:00
a2fb166b55
use IRouteCollection in app container
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-20 00:33:06 +02:00
f0a7b34212
Merge pull request 'feature/update-soko-web' (!28) from feature/update-soko-web into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #28
2023-04-19 23:43:27 +02:00
dccb34971b
adapt Container usage to new soko-web
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-19 23:38:43 +02:00
36f767ea9b
update soko-web version 2023-04-19 23:38:43 +02:00
c0739eeddf
Merge pull request 'feature/activate-audit-logging' (!27) from feature/activate-audit-logging into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #27
2023-04-19 00:58:44 +02:00
cac30c9203
add auditLogger instance to app container
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-18 23:37:55 +02:00
89b0ce768b
add db table for audit log 2023-04-18 23:37:55 +02:00
656ce219ef
add class AuditLogger 2023-04-18 23:37:55 +02:00
fd30911dd3
Merge pull request 'feature/update-to-soko-web-0.5' (!26) from feature/update-to-soko-web-0.5 into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #26
2023-04-18 23:30:33 +02:00
b4cfa7aab2
remove unnecessary startTransaction and commit calls
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-18 23:29:04 +02:00
617ae903ae
add error view 500 to app config 2023-04-18 23:29:04 +02:00
a7997429b7
add view for error 500 2023-04-18 23:26:42 +02:00
4d5e982370
pass dbConnection to HttpResponse 2023-04-18 23:26:42 +02:00
46ea01efe1
update soko-web to 0.5 2023-04-18 23:20:29 +02:00
4161abe098
Merge pull request 'fix path for antiCsrfTokenExceptions' (!25) from bugfix/fix-path-for-csrf-exceptions into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #25
2023-04-16 22:09:15 +02:00
8777d931a4
fix path for antiCsrfTokenExceptions
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 22:07:57 +02:00
d2ef9bb18a
Merge pull request 'remove unnecessary use in web.php' (!24) from bugfix/remove-unnecessary-use into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #24
2023-04-16 22:07:30 +02:00
3ec7fc11fa
remove unnecessary use in web.php
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 22:01:48 +02:00
9fd8453f63
Merge pull request 'feature/adapt-to-new-soko-web' (!23) from feature/adapt-to-new-soko-web into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #23
2023-04-16 21:16:58 +02:00
a7d3942d1f
use classes at beginning of web.php
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 21:14:56 +02:00
41933ec510
update soko-web to 0.4
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 20:54:49 +02:00
e98bb28faf
adapt to soko-web 0.4 2023-04-16 20:54:29 +02:00
34eeb10c27
Merge pull request 'feature/start-using-authentication-required-interface' (!22) from feature/start-using-authentication-required-interface into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #22
2023-04-16 17:53:16 +02:00
70e13f70c0
update soko-web to 0.3
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 17:51:29 +02:00
a89182b64f
split authentication required and secured controllers 2023-04-16 17:51:29 +02:00
45a22c2dd4
Merge pull request 'feature/upgrade-soko-web' (!21) from feature/upgrade-soko-web into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #21
2023-04-16 16:05:51 +02:00
c6d33753c8
adapt to new soko-web
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 16:03:38 +02:00
b32092f1cd
upgrade soko-web to 0.2.1 2023-04-16 16:03:11 +02:00
b5cb59d3ee
Merge pull request 'replace hardcoded links to generated ones' (!20) from feature/replace-hardcoded-links into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #20
2023-04-16 14:44:00 +02:00
0b8d7b9cda
replace hardcoded links to generated ones
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 14:31:10 +02:00
4f4cc43e90
fix home links in LoginController 2023-04-16 14:30:49 +02:00
b066ef22b4
Merge pull request 'add adminer to docker compose stack' (!19) from feature/add-adminer into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #19
2023-04-16 14:24:02 +02:00
08767d1d59
add adminer to docker compose stack
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 14:21:19 +02:00
46404a9d7b
Merge pull request 'fix syntax error in migration' (!18) from hotfix/fix-migration into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #18
2023-04-16 14:07:00 +02:00
8a4c5403cb
fix syntax error in migration
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 14:05:14 +02:00
15a58e8ace
Merge pull request 'implement community basics' (!17) from feature/community-basics into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #17
2023-04-16 14:00:25 +02:00
d763c4344c
implement community basics
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 13:57:54 +02:00
4dc08dffc9
Merge pull request 'feature/fix-oauth-audience' (!16) from feature/fix-oauth-audience into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #16
2023-04-12 02:11:53 +02:00
87b811f716
client id can be anything
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-12 02:10:33 +02:00
eb4ebb9582
send clientId as aud 2023-04-12 02:05:44 +02:00
6af7813e3d
drop audience from oauth_tokens 2023-04-12 02:04:56 +02:00
31ff9b287d
Revert "add aud to jwt"
This reverts commit 97780eb0797a1b05ff0314c64fd1093806ad745d.
2023-04-12 02:03:15 +02:00
c9a06e5ada
Merge pull request 'add aud to jwt' (!15) from feature/add-oauth-audience into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #15
2023-04-12 00:44:52 +02:00
4f007765f4
Merge pull request 'omit whitespace in main layout' (!14) from bugfix/layout-fix into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #14
2023-04-12 00:42:40 +02:00
97780eb079
add aud to jwt
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-12 00:41:20 +02:00
e487a59816
Merge pull request 'restrict oauth access' (!13) from feature/oauth-restrictions into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #13
2023-04-12 00:15:08 +02:00
a7790319eb
restrict oauth access
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-12 00:10:14 +02:00
e143d05801
Merge pull request 'make oauth endpoints openid compliant' (!12) from oauth-improvements into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #12
2023-04-11 19:47:54 +02:00
7c0ebe6668
fix url passed to 'redirect_after_login'
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-11 19:06:22 +02:00
6eb54a6f11
make oauth endpoints openid compliant 2023-04-11 19:06:22 +02:00
ed137b38de
omit whitespace in main layout
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-09 03:16:45 +02:00
db757de71c
Merge pull request 'feature/user-data' (!11) from feature/user-data into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #11
2023-04-09 02:59:35 +02:00
71aed9dcec
send all user data with oauth
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-09 02:55:42 +02:00
b809542083
reload account page after save 2023-04-09 02:55:40 +02:00
749b93e3af
make it possible to modify personal user data fields 2023-04-09 02:55:10 +02:00
df3bf89079
add personal user data fields 2023-04-09 02:54:18 +02:00
de346c0c6e
fix HTML syntax error in account.php 2023-04-09 02:29:53 +02:00
6df63373ab
fix observeInput logic 2023-04-09 02:29:53 +02:00
a0fe77fe66
make it possible to modify email and username 2023-04-09 02:29:53 +02:00
151112bd2a
make it possible to have username 2023-04-09 02:29:46 +02:00
366abf61b3
Merge pull request 'use name 'oauth_payload' for data received from oauth authentication request' (!10) from bugfix/fix-conflicting-session-keys into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #10
2023-04-08 21:25:57 +02:00
367e78cbf8
use name 'oauth_payload' for data received from oauth authentication request
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 21:21:30 +02:00
c056e0bdfc
Merge pull request 'fix redirect again - hopefully last time' (!9) from bugfix/fix-redirects-again into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #9
2023-04-08 21:11:31 +02:00
12890293e0
fix redirect again - hopefully last time
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 20:54:18 +02:00
845f1fe262
Merge pull request 'RVRNEXT-2 disable anti csrf check in case of oauth token' (!8) from feature/RVRNEXT-2-disable-anti-csrf-check-for-oauth-token into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #8
2023-04-08 20:03:20 +02:00
bc9f1a1d1f
RVRNEXT-2 disable anti csrf check in case of oauth token
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 20:02:02 +02:00
84df948012
Merge pull request 'feature/RVRNEXT-2-fixes' (!7) from feature/RVRNEXT-2-fixes into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #7
2023-04-08 19:38:16 +02:00
1200489186
RVRNEXT-2 token endpoint should be POST
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 19:37:05 +02:00
74d0b24f5f
RVRNEXT-2 make endpoint names simpler 2023-04-08 19:37:02 +02:00
cbe6d79ab5
Merge pull request 'feature/RVRNEXT-2-implement-login-to-old-rvr' (!6) from feature/RVRNEXT-2-implement-login-to-old-rvr into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #6
2023-04-08 19:22:09 +02:00
72618c6c66
RVRNEXT-2 ignore *.pem files
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 19:20:47 +02:00
b6018a0715
RVRNEXT-2 add new environment variables for jwt rsa keys 2023-04-08 19:20:47 +02:00
35b7db81b2
RVRNEXT-2 add controller and view for oauth 2023-04-08 19:20:47 +02:00
89c7d3b0ea
RVRNEXT-2 add database accessors for oauth tokens 2023-04-08 19:08:15 +02:00
13b62c8c02
RVRNEXT-2 add new db table for oauth tokens 2023-04-08 19:07:42 +02:00
364d55a4b2
RVRNEXT-2 fix redirect after login 2023-04-08 19:07:18 +02:00
e4dc8ace04
RVRNEXT-2 add new endpoints for oauth 2023-04-08 19:06:45 +02:00
af8ecc748f
RVRNEXT-2 add firebase/php-jwt 2023-04-08 19:06:14 +02:00
6d11be728e
Merge pull request 'login-fixes' (!5) from login-fixes into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #5
2023-04-08 10:50:33 +02:00
453940a5ef
unify redirect after login logic
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 10:45:56 +02:00
d45b790122
show correct error message if google login fails 2023-04-08 03:34:07 +02:00
862daea29c
Merge pull request 'replace constants in app.php instead of main.php' (!4) from fix-update-version-script into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #4
2023-04-08 03:18:49 +02:00
006ffea7c5
replace constants in app.php instead of main.php
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 03:16:28 +02:00
03d71727e8
Merge pull request 'really fix migration' (!3) from fix-migration into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #3
2023-04-08 03:15:25 +02:00
b2584dca31
really fix migration
Some checks are pending
rvr-nextgen/pipeline/pr-master Build queued...
2023-04-08 03:14:36 +02:00
545e5b39ac
Merge pull request 'ignore .gitkeep files in migrations folder' (!2) from fix-migration into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #2
2023-04-08 02:54:09 +02:00
67619926ca
ignore .gitkeep files in migrations folder
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 02:31:22 +02:00
e13281af5f
Merge pull request 'cleanup' (!1) from cleanup into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #1
2023-04-08 02:00:55 +02:00
36d5635812
add empty tests folder
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-08 01:58:55 +02:00
ef6561ac19
remove view test assets 2023-04-08 01:58:27 +02:00
f2aa55bc3e
remove unused styles from css
Some checks failed
rvr-nextgen/pipeline/pr-master There was a failure building this commit
2023-04-08 01:56:35 +02:00
48017bf46f
remove unused variable from js 2023-04-08 01:56:01 +02:00
8edb7ff37f
remove old multi references from install/update scripts 2023-04-08 01:55:34 +02:00
138 changed files with 6093 additions and 1427 deletions

View File

@ -19,3 +19,6 @@ GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
GOOGLE_ANALITICS_ID=your_google_analytics_id GOOGLE_ANALITICS_ID=your_google_analytics_id
RECAPTCHA_SITEKEY=your_recaptcha_sitekey RECAPTCHA_SITEKEY=your_recaptcha_sitekey
RECAPTCHA_SECRET=your_recaptcha_secret RECAPTCHA_SECRET=your_recaptcha_secret
JWT_RSA_PRIVATE_KEY=jwt-rsa256-private.pem
JWT_RSA_PUBLIC_KEY=jwt-rsa256-public.pem
JWT_KEY_KID=1

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
installed installed
vendor vendor
node_modules node_modules
*.pem

65
Jenkinsfile vendored
View File

@ -13,8 +13,9 @@ pipeline {
} }
agent { agent {
dockerfile { dockerfile {
filename 'docker/Dockerfile-test' filename 'docker/Dockerfile'
dir '.' dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true reuseNode true
} }
} }
@ -26,8 +27,9 @@ pipeline {
stage('Unit Testing') { stage('Unit Testing') {
agent { agent {
dockerfile { dockerfile {
filename 'docker/Dockerfile-test' filename 'docker/Dockerfile'
dir '.' dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true reuseNode true
} }
} }
@ -35,7 +37,7 @@ pipeline {
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests' sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
} }
post { post {
success { always {
archiveArtifacts 'unit_test_results.xml' archiveArtifacts 'unit_test_results.xml'
} }
} }
@ -44,8 +46,9 @@ pipeline {
stage('Static Code Analysis') { stage('Static Code Analysis') {
agent { agent {
dockerfile { dockerfile {
filename 'docker/Dockerfile-test' filename 'docker/Dockerfile'
dir '.' dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true reuseNode true
} }
} }
@ -53,10 +56,62 @@ pipeline {
sh 'php vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json' sh 'php vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
} }
post { post {
success { always {
archiveArtifacts 'static_code_analysis_results.json' archiveArtifacts 'static_code_analysis_results.json'
} }
} }
} }
stage('Prepare Docker release') {
environment {
COMPOSER_HOME="${WORKSPACE}/.composer"
npm_config_cache="${WORKSPACE}/.npm"
}
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true
}
}
steps {
script {
sh script: 'git clean -ffdx', label: 'Clean repository'
env.VERSION = sh(script: 'git describe --tags --always --match "Release_*" HEAD', returnStdout: true).trim()
sh script: 'docker/scripts/release.sh', label: 'Release script'
sh script: "rm -rf ${env.COMPOSER_HOME} ${env.npm_config_cache}"
}
}
}
stage('Release Docker image') {
steps {
script {
withDockerRegistry([credentialsId: 'gitea-system-user', url: 'https://git.esoko.eu/']) {
sh script: 'docker buildx create --use --bootstrap --platform=linux/arm64,linux/amd64 --name multi-platform-builder'
sh script: """docker buildx build \
--platform linux/amd64,linux/arm64 \
-f docker/Dockerfile \
--target rvr_release \
-t git.esoko.eu/esoko/rvr:${env.VERSION} \
--push \
.""",
label: 'Build Docker image'
if (env.BRANCH_NAME == 'master') {
if (env.VERSION ==~ '.*-\\d+-g[a-f0-9]{7}') {
env.FIXED_VERSION = 'dev'
} else {
env.FIXED_VERSION = 'stable'
}
sh script: """docker buildx imagetools create \
-t git.esoko.eu/esoko/rvr:${env.FIXED_VERSION} \
git.esoko.eu/esoko/rvr:${env.VERSION}"""
}
}
}
}
}
} }
} }

118
README.md
View File

@ -2,81 +2,81 @@
[![Build Status](https://ci.esoko.eu/job/rvr-nextgen/job/master/badge/icon)](https://ci.esoko.eu/job/rvr-nextgen/job/master/) [![Build Status](https://ci.esoko.eu/job/rvr-nextgen/job/master/badge/icon)](https://ci.esoko.eu/job/rvr-nextgen/job/master/)
This is the RVR Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications. This is the RVR Application project.
## Installation ## Installation
### Clone the Git repository ### Set environment variables
The first step is obviously cloning the repository to your machine: The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment. Check `.env.example` for reference.
**Important: `DEV` should NOT be set for production! See section Development if you want to use the application in development mode.**
### Docker Compose
Create a `docker-compose.yml` file. The example code below assumes that `.env` is placed in the same folder.
```yml
version: '3'
services:
app:
image: git.esoko.eu/esoko/rvr:latest
depends_on:
mariadb:
condition: service_healthy
ports:
- 80:80
volumes:
- .env:/var/www/rvr/.env
mariadb:
image: mariadb:10.3
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: 'root'
MYSQL_DATABASE: 'rvr'
MYSQL_USER: 'rvr'
MYSQL_PASSWORD: 'rvr'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
volumes:
mysql:
```
git clone https://git.esoko.eu/esoko/rvr-nextgen.git
``` ```
All the commands listed here should be executed from the repository root. Execute the following command:
```bash
### Setup Docker stack (recommended)
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
All you have to do is executing the following command:
```
docker compose up -d docker compose up -d
``` ```
Attach shell to the container of `rvr-nextgen-app`:
``` **And you are done!** The application is ready to use. You can create the first administrative user with the following command after attaching to the `app` container:
docker exec -it rvr-nextgen-app bash
```
All of the following commands should be executed there.
### Manual setup (alternative)
If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.
### Initialize project
This command installes all of the Composer requirements and creates a copy of the example `.env` file.
```
composer create-project
```
### Set environment variables
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode.
**Hint:** If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value.
### (Production only) Create cron job
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommended to create a cron job that runs every hour:
```
0 * * * * /path/to/your/installation/rvr db:maintain >>/var/log/cron-rvr.log 2>&1
```
### Finalize installation
After you followed the above steps, execute the following command:
```
scripts/install.sh
```
**And you are done!** The application is ready to use and develop. In development mode an administrative user is also created by the installation script, email is **rvr@rvr.dev**, password is **123456**. In production mode you should create the first administrative user with the following command:
``` ```
./rvr user:add EMAIL PASSWORD admin ./rvr user:add EMAIL PASSWORD admin
``` ```
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306. ## Development
### Set environment variables
`.env.example` should be copied to `.env` into the repo root. Only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value. **`DEV=1` should be set for development!**
### Docker Compose
Execute the following command from the repo root:
```bash
docker compose up -d
```
**And you are done!** You can reach the application on http://localhost. The mails that are sent by the application can be found on http://localhost:8080. If needed, the database server can be directly reached on localhost:3306, or you can use Adminer web interface on http://localhost:9090
You might have to attach to the `app` container, e.g. for creating users, `composer update`, etc.
--- ---

10
app.php
View File

@ -15,9 +15,13 @@ $dotenv->load();
class Container class Container
{ {
static SokoWeb\Interfaces\Database\IConnection $dbConnection; static SokoWeb\Interfaces\Database\IConnection $dbConnection;
static SokoWeb\Routing\RouteCollection $routeCollection; static SokoWeb\Interfaces\Database\IAuditLogger $auditLogger;
static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler; static SokoWeb\Interfaces\PersistentData\IPersistentDataManager $persistentDataManager;
static SokoWeb\Interfaces\Request\IRequest $request; static ?SokoWeb\Interfaces\Routing\IRouteCollection $routeCollection = null;
static ?SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler = null;
static ?SokoWeb\Interfaces\Request\IRequest $request = null;
} }
Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']); Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
Container::$auditLogger = new RVR\Database\AuditLogger(Container::$dbConnection, 'audit_log');
Container::$persistentDataManager = new SokoWeb\PersistentData\PersistentDataManager(Container::$dbConnection, Container::$auditLogger);

View File

@ -10,10 +10,11 @@
} }
], ],
"require": { "require": {
"esoko/soko-web": "0.1" "esoko/soko-web": "0.15",
"firebase/php-jwt": "^6.4"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.6", "phpunit/phpunit": "^10.3",
"phpstan/phpstan": "^1.10" "phpstan/phpstan": "^1.10"
}, },
"autoload": { "autoload": {

1068
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
<?php
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\Currency;
use SokoWeb\Database\Query\Select;
$select = new Select(Container::$dbConnection);
$communities = Container::$persistentDataManager->selectMultipleFromDb($select, Community::class);
foreach ($communities as $community) {
$mainCurrency = new Currency();
$mainCurrency->setCommunity($community);
$mainCurrency->setCode($community->getCurrency());
$mainCurrency->setRoundDigits(0);
Container::$persistentDataManager->saveToDb($mainCurrency);
$community->setMainCurrency($mainCurrency);
Container::$persistentDataManager->saveToDb($community);
}

View File

@ -0,0 +1,12 @@
<?php
use RVR\PersistentData\Model\Community;
use SokoWeb\Database\Query\Select;
$select = new Select(Container::$dbConnection);
$communities = Container::$persistentDataManager->selectMultipleFromDb($select, Community::class);
foreach ($communities as $community) {
$community->generateSlug();
Container::$persistentDataManager->saveToDb($community);
}

View File

@ -0,0 +1,10 @@
CREATE TABLE `oauth_tokens` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`nonce` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
`code` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
`expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `code` (`code`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,3 @@
ALTER TABLE `users`
ADD `username` varchar(100) DEFAULT NULL,
ADD UNIQUE `username` (`username`);

View File

@ -0,0 +1,5 @@
ALTER TABLE `users`
ADD `full_name` varchar(255) NOT NULL DEFAULT '',
ADD `nickname` varchar(255) NOT NULL DEFAULT '',
ADD `phone` varchar(255) NOT NULL DEFAULT '',
ADD `id_number` varchar(255) NOT NULL DEFAULT '';

View File

@ -0,0 +1,4 @@
ALTER TABLE `oauth_tokens`
ADD `scope` varchar(255) NOT NULL DEFAULT '',
ADD `access_token` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
ADD UNIQUE `access_token` (`access_token`);

View File

@ -0,0 +1,10 @@
CREATE TABLE `oauth_clients` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`client_id` varchar(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`client_secret` varchar(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`redirect_uris` text NOT NULL,
`preapproved` tinyint(1) NOT NULL DEFAULT 0,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `client_id` (`client_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,2 @@
ALTER TABLE `oauth_tokens`
ADD `audience` varchar(255) NOT NULL DEFAULT '';

View File

@ -0,0 +1,2 @@
ALTER TABLE `oauth_tokens`
DROP `audience`;

View File

@ -0,0 +1,2 @@
ALTER TABLE `oauth_clients`
MODIFY `client_id` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL;

View File

@ -0,0 +1,20 @@
CREATE TABLE `communities` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`currency` varchar(3) NOT NULL,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `community_members` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`community_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
`owner` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_for_community` (`community_id`, `user_id`),
KEY `community_id` (`community_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `community_members_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`),
CONSTRAINT `community_members_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,12 @@
CREATE TABLE `audit_log` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`local_table` varchar(255) NOT NULL,
`local_id` int(10) unsigned NOT NULL,
`type` enum('insert','update','delete') NOT NULL,
`date` timestamp NOT NULL DEFAULT current_timestamp(),
`modifier_id` int(10) unsigned NULL,
`column` varchar(255) NULL,
`old` text NULL,
`new` text NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@ -0,0 +1,10 @@
CREATE TABLE `currencies` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`community_id` int(10) unsigned NOT NULL,
`code` varchar(3) NOT NULL,
`round_digits` tinyint(1) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_currency_for_community` (`community_id`, `code`),
KEY `community_id` (`community_id`),
CONSTRAINT `currencies_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,10 @@
CREATE TABLE `currency_exchange_rates` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`currency_id` int(10) unsigned NOT NULL,
`exchange_rate` decimal(19,9) unsigned NOT NULL,
`valid_from` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `currency_id` (`currency_id`),
INDEX `valid_from` (`valid_from`),
CONSTRAINT `currency_exchange_rates_currency_id` FOREIGN KEY (`currency_id`) REFERENCES `currencies` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,4 @@
ALTER TABLE `communities`
ADD `main_currency_id` int(10) unsigned DEFAULT NULL,
ADD KEY `main_currency_id` (`main_currency_id`),
ADD CONSTRAINT `communities_main_currency_id` FOREIGN KEY (`main_currency_id`) REFERENCES `currencies` (`id`);

View File

@ -0,0 +1,19 @@
CREATE TABLE `transactions` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`community_id` int(10) unsigned NOT NULL,
`currency_id` int(10) unsigned NOT NULL,
`payer_user_id` int(10) unsigned NOT NULL,
`payee_user_id` int(10) unsigned NULL,
`description` varchar(255) NOT NULL,
`sum` decimal(19,9) NOT NULL,
`time` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `community_id` (`community_id`),
KEY `currency_id` (`currency_id`),
KEY `payer_user_id` (`payer_user_id`),
KEY `payee_user_id` (`payee_user_id`),
CONSTRAINT `transactions_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`),
CONSTRAINT `transactions_currency_id` FOREIGN KEY (`currency_id`) REFERENCES `currencies` (`id`),
CONSTRAINT `transactions_payer_user_id` FOREIGN KEY (`payer_user_id`) REFERENCES `users` (`id`),
CONSTRAINT `transactions_payee_user_id` FOREIGN KEY (`payee_user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,3 @@
ALTER TABLE `communities`
ADD `slug` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL AFTER `id`,
ADD UNIQUE `slug` (`slug`);

View File

@ -0,0 +1,13 @@
CREATE TABLE `events` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`slug` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
`community_id` int(10) unsigned NOT NULL,
`start` timestamp NOT NULL DEFAULT current_timestamp(),
`end` timestamp NOT NULL DEFAULT current_timestamp(),
`title` varchar(255) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`),
KEY `community_id` (`community_id`),
UNIQUE KEY `slug` (`slug`),
CONSTRAINT `events_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,4 @@
ALTER TABLE `transactions`
ADD `event_id` int(10) unsigned NULL AFTER `community_id`,
ADD KEY `event_id` (`event_id`),
ADD CONSTRAINT `transactions_event_id` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`);

View File

@ -0,0 +1,11 @@
CREATE TABLE `transaction_payees` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_for_transaction` (`transaction_id`, `user_id`),
KEY `transaction_id` (`transaction_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `transaction_payees_transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`id`),
CONSTRAINT `transaction_payees_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,26 @@
CREATE TABLE `oauth_sessions` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`client_id` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`scope` varchar(255) NOT NULL DEFAULT '',
`nonce` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
`code` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`code_challenge` varchar(128) NULL,
`code_challenge_method` enum('plain', 'S256') NULL,
`token_claimed` tinyint(1) NOT NULL DEFAULT 0,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
`expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `oauth_tokens`
ADD `session_id` int(10) unsigned NULL,
ADD CONSTRAINT `oauth_tokens_session_id` FOREIGN KEY (`session_id`) REFERENCES `oauth_sessions` (`id`),
DROP INDEX `code`,
DROP INDEX `access_token`,
DROP `scope`,
DROP `nonce`,
DROP `user_id`,
DROP `code`,
DROP `access_token`;

View File

@ -2,12 +2,17 @@ version: '3'
services: services:
app: app:
build: build:
context: ./docker context: .
dockerfile: Dockerfile-app dockerfile: docker/Dockerfile
target: rvr_dev
depends_on:
mariadb:
condition: service_healthy
ports: ports:
- 80:80 - 80:80
volumes: volumes:
- .:/var/www/rvr - .:/var/www/rvr
working_dir: /var/www/rvr
mariadb: mariadb:
image: mariadb:10.3 image: mariadb:10.3
ports: ports:
@ -19,6 +24,19 @@ services:
MYSQL_DATABASE: 'rvr' MYSQL_DATABASE: 'rvr'
MYSQL_USER: 'rvr' MYSQL_USER: 'rvr'
MYSQL_PASSWORD: 'rvr' MYSQL_PASSWORD: 'rvr'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
adminer:
image: adminer:4.8.1-standalone
ports:
- 9090:8080
environment:
- ADMINER_DEFAULT_SERVER=mariadb
mail: mail:
image: marcopas/docker-mailslurper:latest image: marcopas/docker-mailslurper:latest
ports: ports:

44
docker/Dockerfile Normal file
View File

@ -0,0 +1,44 @@
FROM ubuntu:22.04 AS rvr_base
ENV DEBIAN_FRONTEND noninteractive
RUN apt update --fix-missing && apt install -y sudo curl git unzip mariadb-client nginx \
php-apcu php8.1-cli php8.1-curl php8.1-fpm php8.1-mbstring php8.1-mysql php8.1-zip php8.1-xml
RUN mkdir -p /run/php
COPY docker/configs/nginx.conf /etc/nginx/sites-available/default
COPY docker/scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh
COPY docker/scripts/install-nodejs.sh install-nodejs.sh
RUN ./install-nodejs.sh
RUN npm install -g uglify-js clean-css-cli svgo yarn
FROM rvr_base AS rvr_dev
RUN apt update --fix-missing && apt install -y php-xdebug
RUN echo "xdebug.remote_enable = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_autostart = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_connect_back = 1" >> /etc/php/8.1/mods-available/xdebug.ini
EXPOSE 80
EXPOSE 5000
EXPOSE 8090
EXPOSE 9229
ENTRYPOINT docker/scripts/entry-point-dev.sh
FROM rvr_base AS rvr_release
RUN apt update --fix-missing && apt install -y cron
WORKDIR /var/www/rvr
COPY ./ /var/www/rvr
RUN rm -rf /var/www/rvr/.git
EXPOSE 80
EXPOSE 8090
ENTRYPOINT docker/scripts/entry-point.sh

View File

@ -1,30 +0,0 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install Nginx, PHP and further necessary packages
RUN apt update --fix-missing
RUN apt install -y curl git unzip mariadb-client nginx \
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip php7.4-xml
# Configure Nginx with PHP
RUN mkdir -p /run/php
COPY configs/nginx.conf /etc/nginx/sites-available/default
RUN echo "xdebug.remote_enable = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_autostart = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_connect_back = 1" >> /etc/php/7.4/mods-available/xdebug.ini
# Install Composer
COPY scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
RUN npm install -g uglify-js clean-css-cli svgo yarn
EXPOSE 80
VOLUME /var/www/rvr
WORKDIR /var/www/rvr
ENTRYPOINT /usr/sbin/php-fpm7.4 -F & /usr/sbin/nginx -g 'daemon off;'

View File

@ -1,6 +0,0 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
RUN apt update && apt install -y curl git unzip php7.4-cli php7.4-mbstring php7.4-xml
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

View File

@ -1,3 +1,9 @@
map $http_x_forwarded_proto $forwarded_scheme {
default $scheme;
http http;
https https;
}
server { server {
listen 80 default_server; listen 80 default_server;
listen [::]:80 default_server; listen [::]:80 default_server;
@ -14,7 +20,8 @@ server {
location ~ \.php$ { location ~ \.php$ {
include snippets/fastcgi-php.conf; include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param REQUEST_SCHEME $forwarded_scheme;
} }
location ~ /\.ht { location ~ /\.ht {

1
docker/scripts/cron Normal file
View File

@ -0,0 +1 @@
0 * * * * /var/www/rvr/rvr db:maintain

View File

@ -0,0 +1,36 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
if [ -f .env ]; then
composer install
else
composer create-project
fi
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Migrating DB..."
./rvr db:migrate
echo "Set runner user based on owner of .env..."
if ! getent group rvr; then
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID rvr
fi
if ! id -u rvr; then
USER_UID=$(stat -c "%u" .env)
useradd --uid $USER_UID --gid $USER_GID rvr
fi
sed -i -e "s/^user = .*$/user = rvr/g" -e "s/^group = .*$/group = rvr/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
wait -n
exit $?

31
docker/scripts/entry-point.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
set -e
echo "Migrating DB..."
./rvr db:migrate
echo "Installing crontab..."
/usr/bin/crontab docker/scripts/cron
echo "Set runner user based on owner of .env..."
if ! getent group rvr; then
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID rvr
fi
if ! id -u rvr; then
USER_UID=$(stat -c "%u" .env)
useradd --uid $USER_UID --gid $USER_GID rvr
fi
chown -R rvr:rvr cache
sed -i -e "s/^user = .*$/user = rvr/g" -e "s/^group = .*$/group = rvr/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/cron -f &
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
wait -n
exit $?

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
apt update
apt install -y ca-certificates curl gnupg
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=18
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
apt update
apt install -y nodejs

27
docker/scripts/release.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
composer create-project --no-dev
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" app.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" app.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" app.php
echo "Minifying JS, CSS and SVG files..."
find public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
echo "Linking view files..."
./rvr view:link
rm .env

View File

@ -1,9 +0,0 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this Google account ({{EMAIL}}).
<br><br>
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>

View File

@ -1,18 +0,0 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
To activate your account, please click on the following link:<br>
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
<br><br>
You can activate your account until {{ACTIVATABLE_UNTIL}}.
If you don't activate your account, your email address will be permanently deleted after this point of time.
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
However if you want to immediately delete your email address, please click on the following link:<br>
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
<br><br>
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>

View File

@ -1,58 +1,3 @@
<?php <?php
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Response\Redirect;
require '../web.php'; require '../web.php';
$method = strtolower($_SERVER['REQUEST_METHOD']);
$url = substr($_SERVER['REQUEST_URI'], strlen('/'));
if (($pos = strpos($url, '?')) !== false) {
$url = substr($url, 0, $pos);
}
$url = rawurldecode($url);
$match = Container::$routeCollection->match($method, $url == '' ? [] : explode('/', $url));
if ($match !== null) {
list($route, $params) = $match;
Container::$request->setParsedRouteParams($params);
$handler = $route->getHandler();
$controller = new $handler[0](Container::$request);
if ($controller instanceof SokoWeb\Interfaces\Authorization\ISecured) {
$authorized = $controller->authorize();
} else {
$authorized = true;
}
if (!$authorized) {
Container::$request->session()->set('redirect_after_login', $url);
$response = new Redirect(Container::$routeCollection->getRoute('login')->generateLink(), IRedirect::TEMPORARY);
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
return;
}
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
$content = new SokoWeb\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
header('Content-Type: text/html; charset=UTF-8', true, 403);
$content->render();
return;
}
$response = call_user_func([$controller, $handler[1]]);
if ($response instanceof SokoWeb\Interfaces\Response\IContent) {
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
$response->render();
return;
} elseif ($response instanceof SokoWeb\Interfaces\Response\IRedirect) {
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
return;
}
}
$content = new SokoWeb\Response\HtmlContent('error/404');
header('Content-Type: text/html; charset=UTF-8', true, 404);
$content->render();

View File

@ -31,6 +31,10 @@ main {
color: #ffffff; color: #ffffff;
} }
::placeholder, select > option[value=""], .gray {
color: #8e8e8e;
}
p, h1, h2, h3, input, textarea, select, button, a, table, label { p, h1, h2, h3, input, textarea, select, button, a, table, label {
font-family: 'Oxygen', sans-serif; font-family: 'Oxygen', sans-serif;
} }
@ -63,7 +67,7 @@ p, h2, h3 {
line-height: 150%; line-height: 150%;
} }
p { p, th, td {
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
} }
@ -88,18 +92,39 @@ sub {
} }
hr { hr {
border: solid #bbbbbb 1px; border: solid #bbbbcc 1px;
margin: 10px 0; margin: 10px 0;
} }
.normal {
font-weight: 400;
}
.bold { .bold {
font-weight: 700; font-weight: 700;
} }
p.small, span.small { p.small, span.small, td.small {
font-size: 14px; font-size: 14px;
} }
p.big, span.big, td.big {
font-size: 18px;
}
.red {
color: #a80908;
}
.green {
color: #008000;
}
.mono {
font-family: 'Oxygen Mono', mono;
font-weight: 400;
}
.justify { .justify {
text-align: justify; text-align: justify;
} }
@ -109,7 +134,7 @@ p.small, span.small {
} }
.marginLeft { .marginLeft {
margin-left: 10px; margin-left: 5px;
} }
.marginBottom { .marginBottom {
@ -117,7 +142,7 @@ p.small, span.small {
} }
.marginRight { .marginRight {
margin-right: 10px; margin-right: 5px;
} }
.center { .center {
@ -146,6 +171,12 @@ a:hover, a:focus {
text-decoration: underline; text-decoration: underline;
} }
a.block {
color: initial;
font-weight: initial;
text-decoration: initial;
}
button, a.button { button, a.button {
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 16px;
@ -211,6 +242,7 @@ button.noRightRadius, a.button.noRightRadius {
button.gray, a.button.gray { button.gray, a.button.gray {
background-color: #808080; background-color: #808080;
color: #ffffff;
} }
button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus { button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus {
@ -219,6 +251,7 @@ button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.but
button.red, a.button.red { button.red, a.button.red {
background-color: #aa5e5e; background-color: #aa5e5e;
color: #ffffff;
} }
button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus { button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus {
@ -226,11 +259,11 @@ button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button
} }
button.yellow, a.button.yellow { button.yellow, a.button.yellow {
background-color: #e8a349; background-color: #daa520;
} }
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus { button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
background-color: #c37713; background-color: #b8860b;
} }
button.green, a.button.green { button.green, a.button.green {
@ -368,16 +401,20 @@ header>p {
} }
header>p>span { header>p>span {
padding-left: 6px; padding-left: 8px;
} }
header>p>span>a:link, header>p>span>a:visited { header>p>span>a:link, header>p>span>a:visited {
color: inherit; color: inherit;
} }
header>p>span>a:hover, header>p>span>a:focus {
text-decoration: none;
}
header>p>span:not(:last-child) { header>p>span:not(:last-child) {
border-right: solid white 1px; border-right: solid white 1px;
padding-right: 6px; padding-right: 8px;
} }
main { main {
@ -385,13 +422,6 @@ main {
padding: 6px 12px; padding: 6px 12px;
} }
main.full {
position: relative;
width: 100%;
height: calc(100% - 40px);
padding: 0;
}
footer { footer {
background-color: #869ab9; background-color: #869ab9;
padding: 3px 6px; padding: 3px 6px;
@ -411,20 +441,6 @@ div.buttonContainer>button {
margin: 0 auto; margin: 0 auto;
} }
#cookiesNotice {
position: fixed;
left: 0;
bottom: 0;
right: 0;
margin: 20px;
background-color: #eeeeee;
border: solid #888888 1px;
border-radius: 3px;
padding: 10px;
text-align: center;
z-index: 10;
}
#loading { #loading {
position: fixed; position: fixed;
width: 64px; width: 64px;
@ -438,51 +454,106 @@ div.buttonContainer>button {
} }
div.box { div.box {
width: 576px; background-color: #eeeef4;
background-color: #eeeeee;
border-radius: 3px; border-radius: 3px;
margin: 10px auto; margin: 10px auto;
padding: 10px; padding: 10px;
box-sizing: border-box; box-sizing: border-box;
} }
.circleControl { div.compactBox {
position: absolute; width: 576px;
width: 60px;
bottom: 20px;
right: 10px;
} }
.circleControl .controlItem { div.transaction {
position: relative; display: grid;
height: 60px; grid-template-columns: auto auto;
margin-top: 10px;
opacity: 70%;
cursor: pointer;
} }
.circleControl .controlItem:hover { div.gridContainer {
opacity: 100%; display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
grid-gap: 10px;
} }
.circleControl .controlItem div { div.gridContainer > div {
position: absolute; background-color: #eeeef4;
border-radius: 3px;
padding: 5px 10px;
}
table {
border-collapse: separate;
border-spacing: 0;
}
table.fullWidth {
width: 100%; width: 100%;
height: 100%;
} }
.circleControl .controlBackground { table th {
width: 100%; font-weight: 700;
height: 100%;
opacity: 50%;
} }
.circleControl .controlIcon { table th, table td {
width: 75%; padding: 3px 0;
height: 75%; vertical-align: middle;
margin: auto; }
margin-top: 50%;
transform: translateY(-50%); table th:not(:first-child), table td:not(:first-child) {
padding-left: 3px;
}
table th:not(:last-child), table td:not(:last-child) {
padding-right: 3px;
}
p.paginateContainer {
font-size: 0;
}
p.paginateContainer > * {
font-size: initial;
background-color: #5e77aa;
border: solid #5e77aa 1px;
color: #ffffff;
font-weight: 700;
padding: 3px 6px;
text-align: center;
display: inline-block;
height: 25px;
line-height: 25px;
width: 25px;
}
p.paginateContainer > a:hover, p.paginateContainer > .selected {
background-color: #3b5998;
border: solid #29457f 1px;
text-decoration: none;
}
p.paginateContainer > *:not(:last-child) {
border-right: solid #869ab9 1px;
}
p.formLabel {
font-weight: 700;
font-size: 14px;
}
span.label {
border-radius: 3px;
background-color: #555555;
color: #ffffff;
padding: 2px 4px;
font-weight: bold;
font-size: 13px;
}
@media screen and (max-width: 424px) {
div.gridContainer {
grid-template-columns: auto;
}
} }
@media screen and (max-width: 599px) { @media screen and (max-width: 599px) {
@ -493,6 +564,7 @@ div.box {
margin-top: 4px; margin-top: 4px;
} }
button, a.button { button, a.button {
margin: 3px 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
} }
@ -508,15 +580,9 @@ div.box {
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
} }
div.box { div.compactBox {
width: initial; width: initial;
} }
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
} }
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
@ -546,12 +612,6 @@ div.box {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
} }
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
} }
@media screen and (min-height: 400px) and (max-height: 499px) { @media screen and (min-height: 400px) and (max-height: 499px) {

View File

@ -3,7 +3,7 @@ var Account = {
countdown: null, countdown: null,
openGoogleAuthenticate: function () { openGoogleAuthenticate: function () {
window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600') window.open(googleAuthenticateUrl, 'googleAuthenticate', 'height=600,width=600')
}, },
authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) { authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) {
@ -59,11 +59,10 @@ var Account = {
}; };
(function () { (function () {
document.getElementById('authenticateWithGoogleButton').onclick = function () { var authenticateWithGoogleButton = document.getElementById('authenticateWithGoogleButton');
if (authenticateWithGoogleButton) {
authenticateWithGoogleButton.onclick = function () {
Account.openGoogleAuthenticate(); Account.openGoogleAuthenticate();
}; };
}
document.getElementsByTagName('form')[0].onreset = function () {
Account.resetGoogleAuthentication();
};
})(); })();

View File

@ -0,0 +1,30 @@
(function () {
const element = document.getElementById('newMember').elements['user_id'];
const select = new TomSelect(element, {
valueField: 'value',
labelField: 'label',
searchField: 'label',
loadThrottle: 300,
load: function (query, callback) {
var self = this;
RVR.httpRequest('GET', searchUserUrl.replace('QUERY', encodeURIComponent(query)), function () {
self.clearOptions();
callback(this.response.results);
});
},
});
select.on('change', function (value) {
this.clearOptions();
});
select.on('blur', function (value) {
this.clearOptions();
});
select.on('type', function (value) {
if (value === '') {
this.clearOptions();
}
});
})();

View File

@ -0,0 +1,30 @@
(function () {
const element = document.getElementById('transactionForm').elements['event_id'];
const select = new TomSelect(element, {
valueField: 'value',
labelField: 'label',
searchField: 'label',
loadThrottle: 300,
load: function (query, callback) {
var self = this;
RVR.httpRequest('GET', searchEventUrl.replace('QUERY', encodeURIComponent(query)), function () {
self.clearOptions();
callback(this.response.results);
});
},
});
select.on('change', function (value) {
this.clearOptions();
});
select.on('blur', function (value) {
this.clearOptions();
});
select.on('type', function (value) {
if (value === '') {
this.clearOptions();
}
});
})();

View File

@ -1,6 +1,5 @@
var RVR = { var RVR = {
isSecure: window.location.protocol === 'https:', isSecure: window.location.protocol === 'https:',
cookiesAgreed: false,
sessionAvailableHooks: {}, sessionAvailableHooks: {},
initGoogleAnalitics: function () { initGoogleAnalitics: function () {
@ -50,7 +49,7 @@ var RVR = {
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
var formData = new FormData(form); var formData = new FormData(form);
var formError = form.getElementsByClassName('formError')[0]; var formError = document.getElementsByClassName('formError')[0];
var pageLeaveOnSuccess = form.dataset.redirectOnSuccess || form.dataset.reloadOnSuccess; var pageLeaveOnSuccess = form.dataset.redirectOnSuccess || form.dataset.reloadOnSuccess;
RVR.httpRequest('POST', form.action, function () { RVR.httpRequest('POST', form.action, function () {
@ -65,6 +64,9 @@ var RVR = {
formError.style.display = 'block'; formError.style.display = 'block';
formError.innerHTML = this.response.error.errorText; formError.innerHTML = this.response.error.errorText;
if (typeof grecaptcha !== 'undefined') {
grecaptcha.reset();
}
return; return;
} }
@ -89,6 +91,28 @@ var RVR = {
} }
}, },
setOnclickForFormConfirmation: function (button) {
button.onclick = function(e) {
e.preventDefault();
var self = this;
RVR.showModalWithContent('Confirmation', this.dataset.confirmation, [
{
type: 'button',
html: this.dataset.confirmationButton ? this.dataset.confirmationButton : this.innerHTML,
classNames: ['red'],
onclick: function() {
var event = new Event('submit', {'bubbles': true, 'cancelable': true});
self.form.dispatchEvent(event);
RVR.hideModal();
}
}
]);
};
},
showModal: function (id) { showModal: function (id) {
document.getElementById(id).style.visibility = 'visible'; document.getElementById(id).style.visibility = 'visible';
document.getElementById('cover').style.visibility = 'visible'; document.getElementById('cover').style.visibility = 'visible';
@ -126,7 +150,11 @@ var RVR = {
button.classList.add('marginTop'); button.classList.add('marginTop');
button.classList.add('marginRight'); button.classList.add('marginRight');
if (typeof extraButton.html !== 'undefined') {
button.innerHTML = extraButton.html;
} else {
button.textContent = extraButton.text; button.textContent = extraButton.text;
}
if (extraButton.type === 'a') { if (extraButton.type === 'a') {
button.href = extraButton.href; button.href = extraButton.href;
@ -159,12 +187,23 @@ var RVR = {
document.getElementById('cover').style.visibility = 'hidden'; document.getElementById('cover').style.visibility = 'hidden';
}, },
observeInput: function (input, buttonToToggle) { observeInput: function (form, observedInputs) {
if (input.defaultValue !== input.value) { var anyChanged = false;
buttonToToggle.disabled = false;
} else { for (var i = 0; i < observedInputs.length; i++) {
buttonToToggle.disabled = true; var input = form.elements[observedInputs[i]];
if (input.type === 'checkbox') {
if (input.defaultChecked !== input.checked) {
anyChanged = true;
} }
} else {
if (input.defaultValue !== input.value) {
anyChanged = true;
}
}
}
form.elements['submit_button'].disabled = !anyChanged;
}, },
observeInputsInForm: function (form, observedInputs) { observeInputsInForm: function (form, observedInputs) {
@ -175,20 +214,28 @@ var RVR = {
case 'INPUT': case 'INPUT':
case 'TEXTAREA': case 'TEXTAREA':
input.oninput = function () { input.oninput = function () {
RVR.observeInput(this, form.elements.submit); RVR.observeInput(form, observedInputs);
}; };
break; break;
case 'SELECT': case 'SELECT':
input.onchange = function () { input.onchange = function () {
RVR.observeInput(this, form.elements.submit); RVR.observeInput(form, observedInputs);
}; };
break; break;
} }
} }
form.onreset = function () { form.onreset = function () {
form.elements.submit.disabled = true; form.elements['submit_button'].disabled = true;
} }
},
debounce: function(func, timeout = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
} }
}; };
@ -216,6 +263,10 @@ var RVR = {
if (form.dataset.observeInputs) { if (form.dataset.observeInputs) {
RVR.observeInputsInForm(form, form.dataset.observeInputs.split(',')); RVR.observeInputsInForm(form, form.dataset.observeInputs.split(','));
} }
if (form.elements['submit_button'] && form.elements['submit_button'].dataset.confirmation) {
RVR.setOnclickForFormConfirmation(form.elements['submit_button']);
}
} }
document.getElementById('cover').onclick = function () { document.getElementById('cover').onclick = function () {

43
public/static/package-lock.json generated Normal file
View File

@ -0,0 +1,43 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@fortawesome/fontawesome-free": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ=="
},
"@orchidjs/sifter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.0.3.tgz",
"integrity": "sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==",
"requires": {
"@orchidjs/unicode-variants": "^1.0.4"
}
},
"@orchidjs/unicode-variants": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz",
"integrity": "sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ=="
},
"leaflet": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
},
"leaflet.markercluster": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA=="
},
"tom-select": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.2.2.tgz",
"integrity": "sha512-igGah1yY6yhrnN2h/Ky8I5muw/nE/YQxIsEZoYu5qaA4bsRibvKto3s8QZZosKpOd0uO8fNYhRfAwgHB4IAYew==",
"requires": {
"@orchidjs/sifter": "^1.0.3",
"@orchidjs/unicode-variants": "^1.0.4"
}
}
}
}

View File

@ -1,6 +1,8 @@
{ {
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"leaflet": "^1.6.0", "leaflet": "^1.6.0",
"leaflet.markercluster": "^1.4.1" "leaflet.markercluster": "^1.4.1",
"tom-select": "^2.2.2"
} }
} }

View File

@ -2,6 +2,23 @@
# yarn lockfile v1 # yarn lockfile v1
"@fortawesome/fontawesome-free@^6.4.0":
version "6.7.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz#160a48730d533ec77578ed0141661a8f0150a71d"
integrity sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA==
"@orchidjs/sifter@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@orchidjs/sifter/-/sifter-1.1.0.tgz#b36154ad0cda4898305d1ac44f318b41048a0438"
integrity sha512-mYwHCfr736cIWWdhhSZvDbf90AKt2xyrJspKFC3qyIJG1LtrJeJunYEqCGG4Aq2ijENbc4WkOjszcvNaIAS/pQ==
dependencies:
"@orchidjs/unicode-variants" "^1.1.2"
"@orchidjs/unicode-variants@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@orchidjs/unicode-variants/-/unicode-variants-1.1.2.tgz#1fd71791a67fdd1591ebe0dcaadd3964537a824e"
integrity sha512-5DobW1CHgnBROOEpFlEXytED5OosEWESFvg/VYmH0143oXcijYTprRYJTs+55HzGM4IqxiLFSuqEzu9mPNwVsA==
leaflet.markercluster@^1.4.1: leaflet.markercluster@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5" resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5"
@ -11,3 +28,11 @@ leaflet@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ== integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==
tom-select@^2.2.2:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.4.1.tgz#6a0b6df8af3df7b09b22dd965eb75ce4d1c547bc"
integrity sha512-adI8H8+wk8RRzHYLQ3bXSk2Q+FAq/kzAATrcWlJ2fbIrEzb0VkwaXzKHTAlBwSJrhqbPJvhV/0eypFkED/nAug==
dependencies:
"@orchidjs/sifter" "^1.1.0"
"@orchidjs/unicode-variants" "^1.1.2"

3
rvr
View File

@ -9,5 +9,8 @@ $app->add(new RVR\Cli\MigrateDatabaseCommand());
$app->add(new RVR\Cli\AddUserCommand()); $app->add(new RVR\Cli\AddUserCommand());
$app->add(new RVR\Cli\LinkViewCommand()); $app->add(new RVR\Cli\LinkViewCommand());
$app->add(new RVR\Cli\MaintainDatabaseCommand()); $app->add(new RVR\Cli\MaintainDatabaseCommand());
$app->add(new RVR\Cli\AddOAuthClientCommand());
$app->add(new RVR\Cli\AddOAuthRedirectUriCommand());
$app->add(new RVR\Cli\RemoveOAuthRedirectUriCommand());
$app->run(); $app->run();

View File

@ -1,162 +0,0 @@
#!/usr/bin/python3
# Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH
import sys
import os
import subprocess
import re
WORKTREE_REGEX = r"^worktree (.*)\nHEAD ([a-f0-9]*)\n(?:branch refs\/heads\/(.*)|detached)$"
if len(sys.argv) < 4:
print("Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH")
exit(1)
REPO = os.path.abspath(sys.argv[1])
WORKTREE_DEVELOPMENT = os.path.abspath(sys.argv[2])
WORKTREE_PRODUCTION = os.path.abspath(sys.argv[3])
class Worktree:
def __init__(self, path, branch, revision, version):
self.path = path
self.branch = branch
self.revision = revision
self.version = version
self.newRevision = None
self.newVersion = None
def getDataForWorktrees():
ret = subprocess.check_output(["git", "worktree", "list", "--porcelain"], cwd=REPO).decode().strip()
blocks = ret.split("\n\n")
worktrees = []
for block in blocks:
matches = re.search(WORKTREE_REGEX, block)
if matches:
path = matches.group(1)
revision = matches.group(2)
branch = matches.group(3)
version = getVersion(revision)
worktrees.append(Worktree(path, branch, revision, version))
return worktrees
def findWorktree(path):
for worktree in worktrees:
if worktree.path == path:
return worktree
return None
def getVersion(branch):
return subprocess.check_output(["git", "describe", "--tags", "--always", "--match", "Release_*", branch], cwd=REPO).decode().strip()
def getRevisionForRef(ref):
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
def getLatestReleaseTag():
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
for line in process.stdout:
tag = line.decode().rstrip()
if isTagVerified(tag):
return tag
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
raise Exception("No verified 'Release*' tag found!")
def isTagVerified(tag):
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
return process.returncode == 0
def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
def cleanWorktree(worktreePath):
subprocess.call(["git", "clean", "-f", "-d"], cwd=worktreePath)
def updateAppInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath)
def updateAppVersionInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
worktrees = getDataForWorktrees()
updateRepoFromRemote()
print("Repo is updated from origin")
print("----------------------------------------------")
print("----------------------------------------------")
developmentWorktree = findWorktree(WORKTREE_DEVELOPMENT)
developmentWorktree.newRevision = getRevisionForRef(developmentWorktree.branch)
developmentWorktree.newVersion = getVersion(developmentWorktree.revision)
print("DEVELOPMENT (" + developmentWorktree.path + ") is on branch " + developmentWorktree.branch)
print(developmentWorktree.revision + " = " + developmentWorktree.branch + " (" + developmentWorktree.version + ")")
print(developmentWorktree.newRevision + " = origin/" + developmentWorktree.branch + " (" + developmentWorktree.newVersion + ")")
if developmentWorktree.revision != developmentWorktree.newRevision:
print("-> DEVELOPMENT (" + developmentWorktree.path + ") will be UPDATED")
print("----------------------------------------------")
checkoutWorktree(developmentWorktree.path, developmentWorktree.branch)
cleanWorktree(developmentWorktree.path)
print(developmentWorktree.path + " is checked out to " + developmentWorktree.branch + " and cleaned")
updateAppInWorktree(developmentWorktree.path)
updateAppVersionInWorktree(developmentWorktree.path)
print("RVR is updated in " + developmentWorktree.path)
elif developmentWorktree.version != developmentWorktree.newVersion:
print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED")
updateAppVersionInWorktree(developmentWorktree.path)
print("RVR version is updated in " + developmentWorktree.path)
else:
print("-> DEVELOPMENT (" + developmentWorktree.path + ") WON'T be updated")
print("----------------------------------------------")
print("----------------------------------------------")
productionWorktree = findWorktree(WORKTREE_PRODUCTION)
productionWorktree.newVersion = getLatestReleaseTag()
productionWorktree.newRevision = getRevisionForRef(productionWorktree.newVersion)
print("PRODUCTION (" + productionWorktree.path + ")")
print(productionWorktree.revision + " = " + productionWorktree.version)
print(productionWorktree.newRevision + " = " + productionWorktree.newVersion)
if productionWorktree.revision != productionWorktree.newRevision:
print("-> PRODUCTION (" + productionWorktree.path + ") will be UPDATED")
checkoutWorktree(productionWorktree.path, productionWorktree.newRevision)
cleanWorktree(productionWorktree.path)
print(productionWorktree.path + " is checked out to " + productionWorktree.newRevision + " and cleaned")
updateAppInWorktree(productionWorktree.path)
updateAppVersionInWorktree(productionWorktree.path)
print("RVR is updated in " + productionWorktree.path)
else:
print("-> PRODUCTION (" + productionWorktree.path + ") WON'T be updated")
print("----------------------------------------------")
print("----------------------------------------------")

View File

@ -7,7 +7,7 @@ if [[ "${BRANCH_NAME}" =~ $BRANCH_PATTERN ]]; then
TICKET_ID=$(echo $BRANCH_NAME | sed -E "s@$BRANCH_PATTERN@\\2@") TICKET_ID=$(echo $BRANCH_NAME | sed -E "s@$BRANCH_PATTERN@\\2@")
COMMIT_MESSAGE=$(head -n 1 $1) COMMIT_MESSAGE=$(head -n 1 $1)
COMMIT_MESSAGE_REGEX="^$TICKET_ID .*" COMMIT_MESSAGE_REGEX="^(fixup! )?$TICKET_ID .*"
if [[ ! "${COMMIT_MESSAGE}" =~ $COMMIT_MESSAGE_REGEX ]]; then if [[ ! "${COMMIT_MESSAGE}" =~ $COMMIT_MESSAGE_REGEX ]]; then
sed -i.bak -e "1s/^/$TICKET_ID /" $1 sed -i.bak -e "1s/^/$TICKET_ID /" $1

View File

@ -1,35 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -f ${ROOT_DIR}/installed ]; then
echo "RVR is already installed! To force reinstall, delete file 'installed' from the root directory!"
exit 1
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Installing RVR DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/rvr.sql
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./rvr db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./rvr view:link)
else
echo "Creating the first user..."
(cd ${ROOT_DIR} && ./rvr user:add rvr@rvr.dev 123456 admin)
fi
touch ${ROOT_DIR}/installed

View File

@ -1,11 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
find ${ROOT_DIR}/public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find ${ROOT_DIR}/public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find ${ROOT_DIR}/public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;

View File

@ -1,17 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
cd ${ROOT_DIR}
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php

View File

@ -1,29 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
echo "Installing Composer packages..."
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
(cd ${ROOT_DIR} && composer install --no-dev)
else
(cd ${ROOT_DIR} && composer install --dev)
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./rvr db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./rvr view:link)
fi

View File

@ -0,0 +1,52 @@
<?php namespace RVR\Cli;
use DateTime;
use RVR\PersistentData\Model\OAuthClient;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddOAuthClientCommand extends Command
{
public function configure(): void
{
$this->setName('oauth:add-client')
->setDescription('Adding of OAuth client.')
->addArgument('client-id', InputArgument::OPTIONAL, 'Client ID')
->addArgument('preapproved', InputArgument::OPTIONAL, 'Preapproved');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$clientId = $input->getArgument('client-id') ? $input->getArgument('client-id') : bin2hex(random_bytes(8));
$clientSecret = bin2hex(random_bytes(20));
$oAuthClient = new OAuthClient();
$oAuthClient->setClientId($clientId);
$oAuthClient->setClientSecret($clientSecret);
$oAuthClient->setCreatedDate(new DateTime());
if ($input->getArgument('preapproved')) {
$oAuthClient->setPreapproved($input->getArgument('preapproved'));
}
try {
\Container::$persistentDataManager->saveToDb($oAuthClient);
} catch (\Exception $e) {
$output->writeln('<error>Adding OAuth client failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>OAuth client was successfully added!</info>');
$output->writeln('<info>Client ID: ' . $clientId . '</info>');
$output->writeln('<info>Client secret: ' . $clientSecret . '</info>');
return 0;
}
}

View File

@ -0,0 +1,52 @@
<?php namespace RVR\Cli;
use RVR\Repository\OAuthClientRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddOAuthRedirectUriCommand extends Command
{
public function configure(): void
{
$this->setName('oauth:add-redirect-uri')
->setDescription('Adding of redirect URI for OAuth client.')
->addArgument('client_id', InputArgument::REQUIRED, 'The OAuth client ID')
->addArgument('redirect_uris', InputArgument::IS_ARRAY, 'Redirect URIs to add');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$oAuthClientRepository = new OAuthClientRepository();
$oAuthClient = $oAuthClientRepository->getByClientId($input->getArgument('client_id'));
if ($oAuthClient === null) {
$output->writeln('<error>OAuth client does not exist!</error>');
return 1;
}
$redirectUris = array_unique(array_merge($oAuthClient->getRedirectUrisArray(), $input->getArgument('redirect_uris')));
$oAuthClient->setRedirectUrisArray($redirectUris);
try {
\Container::$persistentDataManager->saveToDb($oAuthClient);
} catch (\Exception $e) {
$output->writeln('<error>Adding redirect URI failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$redirectUrisToPrint = [];
foreach ($redirectUris as $redirectUri) $redirectUrisToPrint[] = '* ' . $redirectUri;
$output->writeln('<info>Redirect URIS were successfully added! Current URIs:' . "\n" . implode("\n", $redirectUrisToPrint) . '</info>');
return 0;
}
}

View File

@ -1,7 +1,6 @@
<?php namespace RVR\Cli; <?php namespace RVR\Cli;
use DateTime; use DateTime;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -21,6 +20,11 @@ class AddUserCommand extends Command
public function execute(InputInterface $input, OutputInterface $output): int public function execute(InputInterface $input, OutputInterface $output): int
{ {
if (!filter_var($input->getArgument('email'), FILTER_VALIDATE_EMAIL)) {
$output->writeln('<error>Please provide a valid email address.</error>');
return 1;
}
$user = new User(); $user = new User();
$user->setEmail($input->getArgument('email')); $user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password')); $user->setPlainPassword($input->getArgument('password'));
@ -31,8 +35,7 @@ class AddUserCommand extends Command
} }
try { try {
$pdm = new PersistentDataManager(); \Container::$persistentDataManager->saveToDb($user);
$pdm->saveToDb($user);
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>'); $output->writeln('<error>Adding user failed!</error>');
$output->writeln(''); $output->writeln('');

View File

@ -4,24 +4,28 @@ use DateTime;
use SokoWeb\Database\Query\Modify; use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select; use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet; use SokoWeb\Interfaces\Database\IResultSet;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\Repository\UserPasswordResetterRepository; use RVR\Repository\UserPasswordResetterRepository;
use RVR\Repository\OAuthTokenRepository;
use RVR\Repository\OAuthSessionRepository;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command class MaintainDatabaseCommand extends Command
{ {
private PersistentDataManager $pdm;
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
private OAuthTokenRepository $oauthTokenRepository;
private OAuthSessionRepository $oauthSessionRepository;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->pdm = new PersistentDataManager();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->oauthTokenRepository = new OAuthTokenRepository();
$this->oauthSessionRepository = new OAuthSessionRepository();
} }
public function configure(): void public function configure(): void
@ -35,6 +39,8 @@ class MaintainDatabaseCommand extends Command
try { try {
$this->deleteExpiredPasswordResetters(); $this->deleteExpiredPasswordResetters();
$this->deleteExpiredSessions(); $this->deleteExpiredSessions();
$this->deleteExpiredOauthTokens();
$this->deleteExpiredOauthSessions();
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>'); $output->writeln('<error>Maintenance failed!</error>');
$output->writeln(''); $output->writeln('');
@ -54,7 +60,7 @@ class MaintainDatabaseCommand extends Command
private function deleteExpiredPasswordResetters(): void private function deleteExpiredPasswordResetters(): void
{ {
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) { foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
$this->pdm->deleteFromDb($passwordResetter); \Container::$persistentDataManager->deleteFromDb($passwordResetter);
} }
} }
@ -63,7 +69,7 @@ class MaintainDatabaseCommand extends Command
//TODO: model may be used for sessions too //TODO: model may be used for sessions too
$select = new Select(\Container::$dbConnection, 'sessions'); $select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']); $select->columns(['id']);
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s')); $select->where('updated', '<', (new DateTime('-1 days'))->format('Y-m-d H:i:s'));
$result = $select->execute(); $result = $select->execute();
@ -73,4 +79,21 @@ class MaintainDatabaseCommand extends Command
$modify->delete(); $modify->delete();
} }
} }
private function deleteExpiredOauthTokens(): void
{
foreach ($this->oauthTokenRepository->getAllExpired() as $oauthToken) {
\Container::$persistentDataManager->deleteFromDb($oauthToken);
}
}
private function deleteExpiredOauthSessions(): void
{
foreach ($this->oauthSessionRepository->getAllExpired() as $oauthSession) {
if ($this->oauthTokenRepository->countAllBySession($oauthSession) > 0) {
continue;
}
\Container::$persistentDataManager->deleteFromDb($oauthSession);
}
}
} }

View File

@ -19,6 +19,8 @@ class MigrateDatabaseCommand extends Command
{ {
$db = \Container::$dbConnection; $db = \Container::$dbConnection;
$this->createBaseDb();
$db->startTransaction(); $db->startTransaction();
$success = []; $success = [];
@ -62,10 +64,8 @@ class MigrateDatabaseCommand extends Command
return 0; return 0;
} }
private function readDir(string $type): array private function createBaseDb()
{ {
$done = [];
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*) $migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\' WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
@ -73,6 +73,16 @@ class MigrateDatabaseCommand extends Command
->fetch(IResultSet::FETCH_NUM)[0]; ->fetch(IResultSet::FETCH_NUM)[0];
if ($migrationTableExists != 0) { if ($migrationTableExists != 0) {
return;
}
\Container::$dbConnection->multiQuery(file_get_contents(ROOT . '/database/rvr.sql'));
}
private function readDir(string $type): array
{
$done = [];
$select = new Select(\Container::$dbConnection, 'migrations'); $select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']); $select->columns(['migration']);
$select->where('type', '=', $type); $select->where('type', '=', $type);
@ -83,7 +93,6 @@ class MigrateDatabaseCommand extends Command
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) { while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration']; $done[] = $migration['migration'];
} }
}
$path = ROOT . '/database/migrations/' . $type; $path = ROOT . '/database/migrations/' . $type;
$dir = opendir($path); $dir = opendir($path);
@ -96,7 +105,7 @@ class MigrateDatabaseCommand extends Command
while ($file = readdir($dir)) { while ($file = readdir($dir)) {
$filePath = $path . '/' . $file; $filePath = $path . '/' . $file;
if (!is_file($filePath) || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) { if (!is_file($filePath) || $file == '.gitkeep' || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) {
continue; continue;
} }

View File

@ -0,0 +1,52 @@
<?php namespace RVR\Cli;
use RVR\Repository\OAuthClientRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RemoveOAuthRedirectUriCommand extends Command
{
public function configure(): void
{
$this->setName('oauth:remove-redirect-uri')
->setDescription('Removing of redirect URI for OAuth client.')
->addArgument('client_id', InputArgument::REQUIRED, 'The OAuth client ID')
->addArgument('redirect_uris', InputArgument::IS_ARRAY, 'Redirect URIs to remove');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$oAuthClientRepository = new OAuthClientRepository();
$oAuthClient = $oAuthClientRepository->getByClientId($input->getArgument('client_id'));
if ($oAuthClient === null) {
$output->writeln('<error>OAuth client does not exist!</error>');
return 1;
}
$redirectUris = array_diff($oAuthClient->getRedirectUrisArray(), $input->getArgument('redirect_uris'));
$oAuthClient->setRedirectUrisArray($redirectUris);
try {
\Container::$persistentDataManager->saveToDb($oAuthClient);
} catch (\Exception $e) {
$output->writeln('<error>Removing redirect URI failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$redirectUrisToPrint = [];
foreach ($redirectUris as $redirectUri) $redirectUrisToPrint[] = '* ' . $redirectUri;
$output->writeln('<info>Redirect URIS were successfully removed! Current URIs:' . "\n" . implode("\n", $redirectUrisToPrint) . '</info>');
return 0;
}
}

View File

@ -0,0 +1,470 @@
<?php namespace RVR\Controller;
use DateTime;
use RVR\Finance\BalanceCalculator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\Currency;
use RVR\PersistentData\Model\CurrencyExchangeRate;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityRepository;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\CurrencyExchangeRateRepository;
use RVR\Repository\CurrencyRepository;
use RVR\Repository\EventRepository;
use RVR\Repository\TransactionRepository;
use RVR\Repository\UserRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
class CommunityController implements IAuthenticationRequired
{
private UserRepository $userRepository;
private CommunityRepository $communityRepository;
private CommunityMemberRepository $communityMemberRepository;
private CurrencyRepository $currencyRepository;
private CurrencyExchangeRateRepository $currencyExchangeRatesRepository;
private TransactionRepository $transactionRepository;
private EventRepository $eventRepository;
public function __construct()
{
$this->userRepository = new UserRepository();
$this->communityRepository = new CommunityRepository();
$this->communityMemberRepository = new CommunityMemberRepository();
$this->currencyRepository = new CurrencyRepository();
$this->currencyExchangeRatesRepository = new CurrencyExchangeRateRepository();
$this->transactionRepository = new TransactionRepository();
$this->eventRepository = new EventRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function getCommunityHome(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), false, $community, $ownCommunityMember)) {
return null;
}
\Container::$persistentDataManager->loadRelationsFromDb($community, false, ['main_currency']);
/**
* @var User $user
*/
$user = \Container::$request->user();
$balanceCalculator = new BalanceCalculator($community, $user);
$balance = $balanceCalculator->calculate();
return new HtmlContent('communities/community', [
'community' => $community,
'upcomingAndRecentEvents' => iterator_to_array($this->eventRepository->getUpcomingAndRecentByCommunity($community, new DateTime(), 30, 3)),
'debtItems' => $balance['debtItems'],
'debtBalance' => $balance['debtBalance'],
'outstandingItems' => $balance['outstandingItems'],
'outstandingBalance' => $balance['outstandingBalance'],
'balance' => $balance['absoluteBalance'],
'editPermission' => $ownCommunityMember->getOwner()
]);
}
public function getCommunitySettings(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), false, $community, $ownCommunityMember)) {
return null;
}
return new HtmlContent('communities/community_settings', [
'community' => $community,
'members' => $this->getMembers($community),
'currencies' => $this->getCurrencies($community),
'editPermission' => $ownCommunityMember->getOwner()
]);
}
public function getCommunityNew(): IContent
{
return new HtmlContent('communities/community_edit');
}
public function getCommunityEdit(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
return new HtmlContent('communities/community_edit', [
'community' => $community
]);
}
public function saveCommunity(): ?IContent
{
$name = \Container::$request->post('name');
if (strlen($name) === 0) {
return new JsonContent([
'error' => ['errorText' => 'Please fill all required fields!']
]);
}
$communitySlug = \Container::$request->query('communitySlug');
if ($communitySlug){
if (!$this->checkPermission($communitySlug, true, $community, $ownCommunityMember)) {
return null;
}
} else {
$mainCurrencyCode = \Container::$request->post('main_currency_code');
$mainCurrencyRoundDigits = \Container::$request->post('main_currency_round_digits');
if (strlen($mainCurrencyCode) === 0 || strlen($mainCurrencyCode) > 3 || $mainCurrencyRoundDigits < 0 || $mainCurrencyRoundDigits > 9) {
return new JsonContent([
'error' => ['errorText' => 'Please fill all required fields!']
]);
}
$community = new Community();
$community->setCreatedDate(new DateTime());
}
$community->setName($name);
\Container::$persistentDataManager->saveToDb($community);
if (!$communitySlug) {
/**
* @var User $user
*/
$user = \Container::$request->user();
$communityMember = new CommunityMember();
$communityMember->setCommunity($community);
$communityMember->setUser($user);
$communityMember->setOwner(true);
\Container::$persistentDataManager->saveToDb($communityMember);
$mainCurrency = new Currency();
$mainCurrency->setCommunity($community);
$mainCurrency->setCode($mainCurrencyCode);
$mainCurrency->setRoundDigits($mainCurrencyRoundDigits);
\Container::$persistentDataManager->saveToDb($mainCurrency);
$community->setMainCurrency($mainCurrency);
\Container::$persistentDataManager->saveToDb($community);
}
return new JsonContent([
'redirect' => ['target' => \Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()])]
]);
}
public function deleteCommunity(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
if ($this->transactionRepository->countAllByCommunity($community) > 0) {
return new JsonContent([
'error' => ['errorText' => 'There are transactions for this community!']
]);
}
if ($this->eventRepository->countAllByCommunity($community) > 0) {
return new JsonContent([
'error' => ['errorText' => 'There are events for this community!']
]);
}
foreach ($this->communityMemberRepository->getAllByCommunity($community) as $communityMember) {
\Container::$persistentDataManager->deleteFromDb($communityMember);
}
$community->setMainCurrencyId(null);
\Container::$persistentDataManager->saveToDb($community);
foreach ($this->currencyRepository->getAllByCommunity($community) as $currency) {
foreach ($this->currencyExchangeRatesRepository->getAllByCurrency($currency) as $currencyExchangeRate) {
\Container::$persistentDataManager->deleteFromDb($currencyExchangeRate);
}
\Container::$persistentDataManager->deleteFromDb($currency);
}
\Container::$persistentDataManager->deleteFromDb($community);
return new JsonContent(['success' => true]);
}
public function getMembersEdit(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
return new HtmlContent('communities/community_members', [
'community' => $community,
'members' => $this->getMembers($community)
]);
}
public function saveMember(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
$communityMemberId = \Container::$request->query('community_member_id');
if ($communityMemberId) {
$communityMember = $this->communityMemberRepository->getById($communityMemberId);
if ($communityMember->getUserId() === $ownCommunityMember->getUserId()) {
return new JsonContent([
'error' => ['errorText' => 'Own user cannot be edited.']
]);
}
} else {
if ($this->transactionRepository->isAnyCommon()) {
return new JsonContent([
'error' => ['errorText' => 'There are transactions with common payee!']
]);
}
$user = $this->userRepository->getById(\Container::$request->post('user_id'));
if ($this->communityMemberRepository->getByCommunityAndUser($community, $user) !== null) {
return new JsonContent([
'error' => ['errorText' => 'This user is already a member of this community.']
]);
}
$communityMember = new CommunityMember();
$communityMember->setCommunity($community);
$communityMember->setUser($user);
}
$communityMember->setOwner((bool)\Container::$request->post('owner'));
\Container::$persistentDataManager->saveToDb($communityMember);
return new JsonContent(['success' => true]);
}
public function deleteMember(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
$communityMember = $this->communityMemberRepository->getById(\Container::$request->query('community_member_id'));
if ($communityMember->getUserId() === \Container::$request->user()->getUniqueId()) {
return new JsonContent([
'error' => ['errorText' => 'Own user cannot be deleted.']
]);
}
\Container::$persistentDataManager->loadRelationsFromDb($communityMember, false, ['user']);
if ($this->transactionRepository->isAnyForUser($communityMember->getUser())) {
return new JsonContent([
'error' => ['errorText' => 'There are transactions where the member is payer or payee!']
]);
}
\Container::$persistentDataManager->deleteFromDb($communityMember);
return new JsonContent(['success' => true]);
}
public function getCurrenciesEdit(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
return new HtmlContent('communities/community_currencies', [
'community' => $community,
'currencies' => $this->getCurrencies($community)
]);
}
public function saveCurrency(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
$code = \Container::$request->post('code');
$roundDigits = (int)\Container::$request->post('round_digits');
if (strlen($code) === 0 || strlen($code) > 3 || $roundDigits < 0 || $roundDigits > 9) {
return new JsonContent([
'error' => ['errorText' => 'Please fill all required fields!']
]);
}
$currencyId = \Container::$request->query('currency_id');
if ($currencyId){
$currency = $this->currencyRepository->getById($currencyId);
} else {
$currency = new Currency();
$currency->setCommunity($community);
}
$existingCurrency = $this->currencyRepository->getByCommunityAndCurrencyCode($community, $code);
if ($existingCurrency !== null && $currency->getId() !== $existingCurrency->getId()) {
return new JsonContent([
'error' => ['errorText' => 'A currency with the same code exists for this community.']
]);
}
$currency->setCode($code);
$currency->setRoundDigits($roundDigits);
\Container::$persistentDataManager->saveToDb($currency);
return new JsonContent(['success' => true]);
}
public function deleteCurrency(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
$currency = $this->currencyRepository->getById(\Container::$request->query('currency_id'));
if ($currency->getId() === $community->getMainCurrencyId()) {
return null;
}
if ($this->transactionRepository->isAnyForCurrency($currency)) {
return new JsonContent([
'error' => ['errorText' => 'There are transactions with this currency!']
]);
}
foreach ($this->currencyExchangeRatesRepository->getAllByCurrency($currency) as $currencyExchangeRate) {
\Container::$persistentDataManager->deleteFromDb($currencyExchangeRate);
}
\Container::$persistentDataManager->deleteFromDb($currency);
return new JsonContent(['success' => true]);
}
public function getCurrencyExchangeRates(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
$currency = $this->currencyRepository->getByCommunityAndCurrencyCode($community, \Container::$request->query('code'));
if ($currency === null || $currency->getId() === $community->getMainCurrencyId()) {
return null;
}
$currencyExchangeRates = $this->currencyExchangeRatesRepository->getAllByCurrency($currency);
return new HtmlContent('communities/currency_exchange_rates', [
'community' => $community,
'currency' => $currency,
'currencyExchangeRates' => $currencyExchangeRates,
'editPermission' => $ownCommunityMember->getOwner()
]);
}
public function saveCurrencyExchangeRate(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
$currency = $this->currencyRepository->getByCommunityAndCurrencyCode($community, \Container::$request->query('code'));
if ($currency === null) {
return null;
}
$exchangeRate = (float)\Container::$request->post('exchange_rate');
if ($exchangeRate < 0) {
return new JsonContent([
'error' => ['errorText' => 'Please fill all required fields!']
]);
}
$currencyExchangeRateId = \Container::$request->query('currency_exchange_rate_id');
if ($currencyExchangeRateId){
$currencyExchangeRate = $this->currencyExchangeRatesRepository->getById($currencyExchangeRateId);
} else {
$currencyExchangeRate = new CurrencyExchangeRate();
$currencyExchangeRate->setCurrency($currency);
}
$currencyExchangeRate->setExchangeRate($exchangeRate);
$currencyExchangeRate->setValidFromDate(new DateTime(\Container::$request->post('valid_from')));
\Container::$persistentDataManager->saveToDb($currencyExchangeRate);
return new JsonContent(['success' => true]);
}
public function deleteCurrencyExchangeRate(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
$currency = $this->currencyRepository->getByCommunityAndCurrencyCode($community, \Container::$request->query('code'));
if ($currency === null) {
return null;
}
$currencyExchangeRate = $this->currencyExchangeRatesRepository->getById(\Container::$request->query('currency_exchange_rate_id'));
\Container::$persistentDataManager->deleteFromDb($currencyExchangeRate);
return new JsonContent(['success' => true]);
}
private function getMembers(Community $community): array
{
$members = iterator_to_array($this->communityMemberRepository->getAllByCommunity($community, true, ['user']));
usort($members, function($a, $b) {
return strnatcmp($a->getUser()->getDisplayName(), $b->getUser()->getDisplayName());
});
return $members;
}
private function getCurrencies(Community $community): array
{
$currencies = iterator_to_array($this->currencyRepository->getAllByCommunity($community));
usort($currencies, function($a, $b) {
return strnatcmp($a->getCode(), $b->getCode());
});
usort($currencies, function($a, $b) use ($community) {
return (int)($b->getId() === $community->getMainCurrencyId()) - (int)($a->getId() === $community->getMainCurrencyId());
});
return $currencies;
}
private function checkPermission(
string $communitySlug,
bool $needToBeOwner,
?Community &$community,
?CommunityMember &$ownCommunityMember): bool
{
$community = $this->communityRepository->getBySlug($communitySlug);
if ($community === null) {
return false;
}
/**
* @var User $user
*/
$user = \Container::$request->user();
$ownCommunityMember = $this->communityMemberRepository->getByCommunityAndUser($community, $user);
if ($ownCommunityMember === null || ($needToBeOwner && !$ownCommunityMember->getOwner())) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,193 @@
<?php namespace RVR\Controller;
use Container;
use DateTime;
use RVR\Finance\BalanceCalculator;
use RVR\Finance\ExchangeRateCalculator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\CommunityRepository;
use RVR\Repository\EventRepository;
use RVR\Repository\TransactionRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
class EventController implements IAuthenticationRequired, ISecured
{
private CommunityRepository $communityRepository;
private CommunityMemberRepository $communityMemberRepository;
private EventRepository $eventRepository;
private TransactionRepository $transactionRepository;
private ?Community $community;
private ?CommunityMember $ownCommunityMember;
public function __construct()
{
$this->communityRepository = new CommunityRepository();
$this->communityMemberRepository = new CommunityMemberRepository();
$this->eventRepository = new EventRepository();
$this->transactionRepository = new TransactionRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function authorize(): bool
{
$communitySlug = \Container::$request->query('communitySlug');
$this->community = $this->communityRepository->getBySlug($communitySlug);
if ($this->community === null) {
return false;
}
/**
* @var User $user
*/
$user = \Container::$request->user();
$this->ownCommunityMember = $this->communityMemberRepository->getByCommunityAndUser($this->community, $user);
if ($this->ownCommunityMember === null) {
return false;
}
return true;
}
public function getEvents(): IContent
{
$itemsPerPage = 10;
$numberOfEvents = $this->eventRepository->countAllByCommunity($this->community);
$currentPage = Container::$request->query('page') ?: 1;
$events = $this->eventRepository->getPagedByCommunity(
$this->community,
$currentPage,
$itemsPerPage
);
return new HtmlContent('events/events', [
'community' => $this->community,
'pages' => ceil($numberOfEvents / $itemsPerPage),
'currentPage' => $currentPage,
'numberOfEvents' => $numberOfEvents,
'events' => $events
]);
}
public function searchEvent(): IContent
{
$events = iterator_to_array($this->eventRepository->searchByTitle($this->community, Container::$request->query('q')));
$results = [];
foreach ($events as $event) {
$results[] = ['value' => $event->getId(), 'label' => $event->getTitle()];
}
return new JsonContent([
'results' => $results
]);
}
public function getEvent(): ?IContent
{
$event = $this->eventRepository->getBySlug(Container::$request->query('eventSlug'));
if (!$event) {
return null;
}
Container::$persistentDataManager->loadRelationsFromDb($this->community, true, ['main_currency']);
/**
* @var User $user
*/
$user = \Container::$request->user();
$balanceCalculator = new BalanceCalculator($this->community, $user, $event);
$balance = $balanceCalculator->calculate();
return new HtmlContent('events/event', [
'community' => $this->community,
'event' => $event,
'totalCost' => $this->sumTransactions($event),
'debtItems' => $balance['debtItems'],
'debtBalance' => $balance['debtBalance'],
'outstandingItems' => $balance['outstandingItems'],
'outstandingBalance' => $balance['outstandingBalance'],
'balance' => $balance['absoluteBalance'],
]);
}
public function getEventEdit(): ?IContent
{
$eventSlug = Container::$request->query('eventSlug');
if ($eventSlug) {
$event = $this->eventRepository->getBySlug($eventSlug);
if ($event === null) {
return null;
}
} else {
$event = null;
}
return new HtmlContent('events/event_edit', [
'community' => $this->community,
'event' => $event
]);
}
public function saveEvent(): ?IContent
{
$eventSlug = Container::$request->query('eventSlug');
if ($eventSlug) {
$event = $this->eventRepository->getBySlug($eventSlug);
} else {
$event = new Event();
$event->setCommunity($this->community);
}
$event->setTitle(Container::$request->post('title'));
$event->setDescription(Container::$request->post('description'));
$event->setStartDate(new DateTime(Container::$request->post('start')));
$event->setEndDate(new DateTime(Container::$request->post('end')));
Container::$persistentDataManager->saveToDb($event);
return new JsonContent([
'redirect' => ['target' => \Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $this->community->getSlug(), 'eventSlug' => $event->getSlug()])]
]);
}
public function deleteEvent(): IContent
{
$event = $this->eventRepository->getBySlug(Container::$request->query('eventSlug'));
foreach ($this->transactionRepository->getAllByEvent($event) as $transaction) {
$transaction->setEventId(null);
Container::$persistentDataManager->saveToDb($transaction);
}
Container::$persistentDataManager->deleteFromDb($event);
return new JsonContent(['success' => true]);
}
private function sumTransactions(Event $event): float
{
$exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
$transactions = $this->transactionRepository->getAllByEvent($event, true, ['currency']);
$sum = 0.0;
foreach ($transactions as $transaction) {
$sum += $exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate());
}
return $sum;
}
}

View File

@ -0,0 +1,70 @@
<?php namespace RVR\Controller;
use DateTime;
use Container;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\User;
use RVR\Repository\EventRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\Redirect;
class EventRedirectController implements IAuthenticationRequired
{
private EventRepository $eventRepository;
public function __construct()
{
$this->eventRepository = new EventRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function getEvent()
{
$currentEvent = $this->getCurrentEvent();
if ($currentEvent === null) {
return new HtmlContent('event_redirect/no_event');
}
return new Redirect(
\Container::$routeCollection->getRoute('community.event')
->generateLink([
'communitySlug' => $currentEvent->getCommunity()->getSlug(),
'eventSlug' => $currentEvent->getSlug()
]),
IRedirect::TEMPORARY
);
}
public function getEventNewTransaction()
{
$currentEvent = $this->getCurrentEvent();
if ($currentEvent === null) {
return new HtmlContent('event_redirect/no_event');
}
return new Redirect(
\Container::$routeCollection->getRoute('community.transactions.new')
->generateLink([
'communitySlug' => $currentEvent->getCommunity()->getSlug(),
'event' => $currentEvent->getSlug()
]),
IRedirect::TEMPORARY
);
}
private function getCurrentEvent(): ?Event
{
/**
* @var User $user
*/
$user = Container::$request->user();
return $this->eventRepository->getCurrentByUser($user, new DateTime(), 30, true, ['community']);
}
}

View File

@ -1,27 +1,50 @@
<?php namespace RVR\Controller; <?php namespace RVR\Controller;
use SokoWeb\Interfaces\Authorization\ISecured; use DateTime;
use SokoWeb\Interfaces\Request\IRequest; use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\EventRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent; use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
class HomeController implements ISecured class HomeController implements IAuthenticationRequired
{ {
private IRequest $request; private CommunityMemberRepository $communityMemberRepository;
public function __construct(IRequest $request) private EventRepository $eventRepository;
public function __construct()
{ {
$this->request = $request; $this->communityMemberRepository = new CommunityMemberRepository();
$this->eventRepository = new EventRepository();
} }
public function authorize(): bool public function isAuthenticationRequired(): bool
{ {
return $this->request->user() !== null; return true;
} }
public function getIndex(): IContent public function getHome(): IContent
{ {
return new HtmlContent('index'); /**
* @var User $user
*/
$user = \Container::$request->user();
$ownCommunityMembers = $this->communityMemberRepository->getAllByUser($user, true, ['community']);
$communities = [];
foreach ($ownCommunityMembers as $ownCommunityMember) {
$communities[] = $ownCommunityMember->getCommunity();
}
usort($communities, function($a, $b) {
return strnatcmp($a->getName(), $b->getName());
});
return new HtmlContent('home', [
'communities' => $communities,
'upcomingAndRecentEvents' => iterator_to_array($this->eventRepository->getUpcomingAndRecentByUser($user, new DateTime(), 30, 3, true, ['community']))
]);
} }
} }

View File

@ -2,13 +2,11 @@
use DateTime; use DateTime;
use SokoWeb\Http\Request; use SokoWeb\Http\Request;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect; use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Mailing\Mail; use SokoWeb\Mailing\Mail;
use SokoWeb\OAuth\GoogleOAuth; use SokoWeb\OAuth\GoogleOAuth;
use RVR\PersistentData\Model\UserPasswordResetter; use RVR\PersistentData\Model\UserPasswordResetter;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\Repository\UserPasswordResetterRepository; use RVR\Repository\UserPasswordResetterRepository;
use RVR\Repository\UserRepository; use RVR\Repository\UserRepository;
use SokoWeb\Response\HtmlContent; use SokoWeb\Response\HtmlContent;
@ -19,29 +17,29 @@ use SokoWeb\Util\JwtParser;
class LoginController class LoginController
{ {
private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository; private UserRepository $userRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request) private string $redirectUrl;
public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->redirectUrl = \Container::$request->session()->has('redirect_after_login') ?
\Container::$request->session()->get('redirect_after_login') :
\Container::$routeCollection->getRoute('home')->generateLink();
} }
public function getLoginForm() public function getLoginForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
return new HtmlContent('login/login'); return new HtmlContent('login/login', ['redirectUrl' => $this->redirectUrl]);
} }
public function getGoogleLoginRedirect(): IRedirect public function getGoogleLoginRedirect(): IRedirect
@ -49,13 +47,13 @@ class LoginController
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16)); $nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state); \Container::$request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce); \Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl( $url = $oAuth->getDialogUrl(
$state, $state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(), \Container::$request->getBase() . \Container::$routeCollection->getRoute('login.google-action')->generateLink(),
$nonce $nonce
); );
@ -64,11 +62,12 @@ class LoginController
public function getRequestPasswordResetForm() public function getRequestPasswordResetForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]); return new HtmlContent('login/password_reset_request', ['email' => \Container::$request->query('email')]);
} }
public function getRequestPasswordResetSuccess(): IContent public function getRequestPasswordResetSuccess(): IContent
@ -78,11 +77,12 @@ class LoginController
public function getResetPasswordForm() public function getResetPasswordForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
$token = $this->request->query('token'); $token = \Container::$request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token); $resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) { if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
@ -91,112 +91,108 @@ class LoginController
$user = $this->userRepository->getById($resetter->getUserId()); $user = $this->userRepository->getById($resetter->getUserId());
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]); return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail(), 'redirectUrl' => $this->redirectUrl]);
} }
public function login(): IContent public function login(): IContent
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
$user = $this->userRepository->getByEmail($this->request->post('email')); $user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
if ($user === null || !$user->checkPassword($this->request->post('password'))) { if ($user === null || !$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'No user found with the given email address or the given password is wrong. You can <a href="/password/requestReset?email=' . 'errorText' => 'No user found with the given email address / username or the given password is wrong. You can <a href="' .
urlencode($this->request->post('email')) . '" title="Request password reset">request password reset</a>!' \Container::$routeCollection->getRoute('password.requestReset')->generateLink(['email' => \Container::$request->post('email')]) . '" title="Request password reset">request password reset</a>!'
] ]
]); ]);
} }
$this->request->setUser($user); \Container::$request->setUser($user);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function loginWithGoogle() public function loginWithGoogle()
{ {
$redirectUrl = $this->request->session()->get('redirect_after_login'); $defaultError = 'Authentication with Google failed. Please <a href="' . \Container::$routeCollection->getRoute('login.google')->generateLink() . '" title="Login with Google">try again</a>!';
if ($redirectUrl === null) {
$redirectUrl = \Container::$routeCollection->getRoute('index')->generateLink(); if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
if ($this->request->user() !== null) { if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
$this->request->session()->delete('redirect_after_login'); return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
return new Redirect($redirectUrl, IRedirect::TEMPORARY);
}
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
return new HtmlContent('login/google_login');
} }
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken( $tokenData = $oAuth->getToken(
$this->request->query('code'), \Container::$request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink() \Container::$request->getBase() . \Container::$routeCollection->getRoute('login.google-action')->generateLink()
); );
if (!isset($tokenData['id_token'])) { if (!isset($tokenData['id_token'])) {
return new HtmlContent('login/google_login'); return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
} }
$jwtParser = new JwtParser($tokenData['id_token']); $jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload(); $idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) { if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
return new HtmlContent('login/google_login'); return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
} }
if (!$idToken['email_verified']) { if (!$idToken['email_verified']) {
return new HtmlContent('login/google_login'); return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
} }
$user = $this->userRepository->getByGoogleSub($idToken['sub']); $user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) { if ($user === null) {
return new JsonContent([ return new HtmlContent('login/google_login_error', ['error' => 'No user found for this Google account.']);
'error' => [
'errorText' => 'No user found for this Google account.'
]
]);
} }
$this->request->setUser($user); \Container::$request->setUser($user);
$this->request->session()->delete('redirect_after_login'); $this->deleteRedirectUrl();
return new Redirect($redirectUrl, IRedirect::TEMPORARY); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
public function logout(): IRedirect public function logout(): IRedirect
{ {
$this->request->setUser(null); \Container::$request->setUser(null);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('home')->generateLink(), IRedirect::TEMPORARY);
} }
public function requestPasswordReset(): IContent public function requestPasswordReset(): IContent
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink() 'target' => $this->redirectUrl
] ]
]); ]);
} }
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) { if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
if (!$this->request->post('g-recaptcha-response')) { if (!\Container::$request->post('g-recaptcha-response')) {
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]); return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
} }
$captchaValidator = new CaptchaValidator(); $captchaValidator = new CaptchaValidator();
$captchaResponse = $captchaValidator->validate($this->request->post('g-recaptcha-response')); $captchaResponse = $captchaValidator->validate(\Container::$request->post('g-recaptcha-response'));
if (!$captchaResponse['success']) { if (!$captchaResponse['success']) {
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]); return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
} }
} }
$user = $this->userRepository->getByEmail($this->request->post('email')); $user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
if ($user === null) { if ($user === null) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
@ -222,15 +218,11 @@ class LoginController
$passwordResetter->setToken($token); $passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires); $passwordResetter->setExpiresDate($expires);
\Container::$dbConnection->startTransaction();
if ($existingResetter !== null) { if ($existingResetter !== null) {
$this->pdm->deleteFromDb($existingResetter); \Container::$persistentDataManager->deleteFromDb($existingResetter);
} }
$this->pdm->saveToDb($passwordResetter); \Container::$persistentDataManager->saveToDb($passwordResetter);
\Container::$dbConnection->commit();
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires); $this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
@ -239,26 +231,27 @@ class LoginController
public function resetPassword(): IContent public function resetPassword(): IContent
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink() 'target' => $this->redirectUrl
] ]
]); ]);
} }
$token = $this->request->query('token'); $token = \Container::$request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token); $resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) { if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]) 'target' => \Container::$routeCollection->getRoute('password.reset')->generateLink(['token' => $token])
] ]
]); ]);
} }
if (strlen($this->request->post('password')) < 6) { if (strlen(\Container::$request->post('password')) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
@ -266,23 +259,20 @@ class LoginController
]); ]);
} }
if ($this->request->post('password') !== $this->request->post('password_confirm')) { if (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]); return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
} }
\Container::$dbConnection->startTransaction(); \Container::$persistentDataManager->deleteFromDb($resetter);
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId()); $user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword($this->request->post('password')); $user->setPlainPassword(\Container::$request->post('password'));
$this->pdm->saveToDb($user); \Container::$persistentDataManager->saveToDb($user);
\Container::$dbConnection->commit(); \Container::$request->setUser($user);
$this->request->setUser($user);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
@ -293,10 +283,15 @@ class LoginController
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset'); $mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [ $mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email, 'EMAIL' => $email,
'RESET_LINK' => $this->request->getBase() . '/' . 'RESET_LINK' => \Container::$request->getBase() .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]), \Container::$routeCollection->getRoute('password.reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T') 'EXPIRES' => $expires->format('Y-m-d H:i T')
]); ]);
$mail->send(); $mail->send();
} }
private function deleteRedirectUrl(): void
{
\Container::$request->session()->delete('redirect_after_login');
}
} }

View File

@ -0,0 +1,383 @@
<?php namespace RVR\Controller;
use DateTime;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
use Firebase\JWT\ExpiredException;
use RVR\Repository\OAuthSessionRepository;
use RVR\Repository\OAuthTokenRepository;
use RVR\Repository\UserRepository;
use RVR\PersistentData\Model\User;
use RVR\PersistentData\Model\OAuthSession;
use RVR\PersistentData\Model\OAuthToken;
use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent;
class OAuthController
{
private OAuthClientRepository $oAuthClientRepository;
private OAuthSessionRepository $oAuthSessionRepository;
private OAuthTokenRepository $oAuthTokenRepository;
private UserRepository $userRepository;
public function __construct()
{
$this->oAuthClientRepository = new OAuthClientRepository();
$this->oAuthSessionRepository = new OAuthSessionRepository();
$this->oAuthTokenRepository = new OAuthTokenRepository();
$this->userRepository = new UserRepository();
}
public function generateToken(): ?IContent
{
$credentials = $this->getClientCredentials();
$code = \Container::$request->post('code');
$redirectUri = \Container::$request->post('redirect_uri');
if (!$credentials['clientId'] || !$code || !$redirectUri) {
return new JsonContent([
'error' => 'An invalid request was made.'
]);
}
$client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
if ($client === null) {
return new JsonContent([
'error' => 'Client is not found.'
]);
}
$redirectUriBase = explode('?', $redirectUri)[0];
if (!in_array($redirectUriBase, $client->getRedirectUrisArray())) {
return new JsonContent([
'error' => 'Redirect URI \'' . $redirectUriBase .'\' is not allowed for this client.'
]);
}
$session = $this->oAuthSessionRepository->getByCode($code);
if ($session === null || $session->getTokenClaimed() || $session->getExpiresDate() < new DateTime()) {
return new JsonContent([
'error' => 'The provided code is invalid.'
]);
}
$codeChallenge = $session->getCodeChallenge();
if ($codeChallenge === null && $client->getClientSecret() !== $credentials['clientSecret']) {
return new JsonContent([
'error' => 'Client is not authorized.'
]);
}
if ($session->getClientId() !== $credentials['clientId']) {
return new JsonContent([
'error' => 'This code cannot be used by this client!'
]);
}
if ($codeChallenge !== null) {
$codeVerifier = \Container::$request->post('code_verifier') ?: '';
if ($session->getCodeChallengeMethod() === 'S256') {
$hash = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
} else {
$hash = $codeVerifier;
}
if ($codeChallenge !== $hash) {
return new JsonContent([
'error' => 'Code challenge failed!'
]);
}
}
$session->setTokenClaimed(true);
\Container::$persistentDataManager->saveToDb($session);
$token = new OAuthToken();
$token->setSession($session);
$token->setCreatedDate(new DateTime());
$token->setExpiresDate(new DateTime('+1 hours'));
\Container::$persistentDataManager->saveToDb($token);
$commonPayload = [
'iss' => $_ENV['APP_URL'],
'iat' => $token->getCreatedDate()->getTimestamp(),
'nbf' => $session->getCreatedDate()->getTimestamp(),
'exp' => $token->getExpiresDate()->getTimestamp(),
'aud' => $session->getClientId(),
'nonce' => $session->getNonce()
];
$idTokenPayload = array_merge($commonPayload, $this->getUserInfoInternal(
$this->userRepository->getById($session->getUserId()),
$session->getScopeArray())
);
$accessTokenPayload = array_merge($commonPayload, [
'jti' => $token->getId(),
]);
$privateKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PRIVATE_KEY']);
$idToken = JWT::encode($idTokenPayload, $privateKey, 'RS256', $_ENV['JWT_KEY_KID']);
$accessToken = JWT::encode($accessTokenPayload, $privateKey, 'RS256', $_ENV['JWT_KEY_KID']);
return new JsonContent([
'access_token' => $accessToken,
'expires_in' => $token->getExpiresDate()->getTimestamp() - (new DateTime())->getTimestamp(),
'scope' => $session->getScope(),
'id_token' => $idToken,
'token_type' => 'Bearer'
]);
}
public function introspectToken(): ?IContent
{
$credentials = $this->getClientCredentials();
$accessToken = \Container::$request->post('token');
if (!$credentials['clientId'] || !$credentials['clientSecret'] || !$accessToken) {
return new JsonContent([
'error' => 'An invalid request was made.'
]);
}
$client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
if ($client === null || $client->getClientSecret() !== $credentials['clientSecret']) {
return new JsonContent([
'error' => 'Client is not authorized.'
]);
}
$tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
if (!$tokenValidated) {
return new JsonContent([
'active' => false
]);
}
if ($session->getClientId() !== $credentials['clientId']) {
return new JsonContent([
'active' => false
]);
}
return new JsonContent([
'active' => true,
'scope' => $session->getScope(),
'client_id' => $session->getClientId(),
'exp' => $token->getExpiresDate()->getTimestamp(),
]);
}
public function revokeToken(): ?IContent
{
$credentials = $this->getClientCredentials();
$accessToken = \Container::$request->post('token');
if (!$credentials['clientId'] || !$credentials['clientSecret'] || !$accessToken) {
return new JsonContent([
'error' => 'An invalid request was made.'
]);
}
$client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
if ($client === null || $client->getClientSecret() !== $credentials['clientSecret']) {
return new JsonContent([
'error' => 'Client is not authorized.'
]);
}
$tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
if (!$tokenValidated) {
return new JsonContent([]);
}
$session = $this->oAuthSessionRepository->getById($token->getSessionId());
if ($session->getClientId() !== $credentials['clientId']) {
return new JsonContent([]);
}
\Container::$persistentDataManager->deleteFromDb($token);
return new JsonContent([]);
}
public function getUserInfo() : IContent
{
$authorization = \Container::$request->header('Authorization');
if ($authorization === null) {
return new JsonContent([
'error' => 'No Authorization header was sent.'
]);
}
$accessToken = substr($authorization, strlen('Bearer '));
$tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
if (!$tokenValidated) {
return new JsonContent([
'error' => 'The provided access token is invalid.'
]);
}
return new JsonContent(
$this->getUserInfoInternal(
$this->userRepository->getById($session->getUserId()),
$session->getScopeArray()
)
);
}
public function getConfig(): IContent
{
return new JsonContent([
'issuer' => $_ENV['APP_URL'],
'authorization_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.auth')->generateLink(),
'token_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token')->generateLink(),
'introspection_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token.introspect')->generateLink(),
'revocation_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token.revoke')->generateLink(),
'userinfo_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.userinfo')->generateLink(),
'end_session_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('logout')->generateLink(),
'jwks_uri' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.certs')->generateLink(),
'response_types_supported' =>
[
'code',
],
'subject_types_supported' =>
[
'public',
],
'id_token_signing_alg_values_supported' =>
[
'RS256',
],
'scopes_supported' =>
[
'openid',
'email',
'profile',
],
'token_endpoint_auth_methods_supported' =>
[
'client_secret_basic',
'client_secret_post',
],
'claims_supported' =>
[
'aud',
'email',
'exp',
'full_name',
'iat',
'id_number',
'iss',
'nickname',
'phone',
'picture',
'sub',
'username',
],
'code_challenge_methods_supported' =>
[
'plain',
'S256',
],
'grant_types_supported' =>
[
'authorization_code',
],
]);
}
public function getCerts(): IContent
{
$publicKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PUBLIC_KEY']);
$keyInfo = openssl_pkey_get_details(openssl_pkey_get_public($publicKey));
return new JsonContent(['keys' => [
[
'kty' => 'RSA',
'alg' => 'RS256',
'use' => 'sig',
'kid' => $_ENV['JWT_KEY_KID'],
'n' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['n'])),
'e' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['e'])),
]
]]);
}
private function getClientCredentials(): array
{
$authorization = \Container::$request->header('Authorization');
if ($authorization !== null) {
$basicAuthEncoded = substr($authorization, strlen('Basic '));
$basicAuth = explode(':', base64_decode($basicAuthEncoded));
if (count($basicAuth) === 2) {
$clientId = rawurldecode($basicAuth[0]);
$clientSecret = rawurldecode($basicAuth[1]);
} else {
$clientId = null;
$clientSecret = null;
}
} else {
$clientId = \Container::$request->post('client_id');
$clientSecret = \Container::$request->post('client_secret');
}
return ['clientId' => $clientId, 'clientSecret' => $clientSecret];
}
private function validateTokenAndSession(
string $accessToken,
?OAuthToken &$token,
?OAuthSession &$session): bool
{
$publicKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PUBLIC_KEY']);
try {
$payload = JWT::decode($accessToken, new Key($publicKey, 'RS256'));
$token = $this->oAuthTokenRepository->getById($payload->jti);
} catch (SignatureInvalidException | BeforeValidException | ExpiredException) {
$token = null;
} catch (\UnexpectedValueException $e) {
error_log($e->getMessage() . ' Token was: ' . $accessToken);
$token = null;
}
if ($token === null || $token->getExpiresDate() < new DateTime()) {
return false;
}
$session = $this->oAuthSessionRepository->getById($token->getSessionId());
if ($session === null) {
return false;
}
return true;
}
/**
* @param User $user
* @param string[] $scope
* @return array<string, string>
*/
private function getUserInfoInternal(User $user, array $scope): array
{
$userInfo = [];
if (in_array('openid', $scope)) {
$userInfo['sub'] = (string)$user->getId();
}
if (in_array('email', $scope)) {
$userInfo['email'] = $user->getEmail();
}
if (in_array('profile', $scope)) {
if ($user->getUsername() !== null) {
$userInfo['preferred_username'] = $user->getUsername();
}
$userInfo['name'] = $user->getFullName();
$userInfo['nickname'] = $user->getNickname();
$userInfo['phone_number'] = $user->getPhone();
$userInfo['id_number'] = $user->getIdNumber();
}
return $userInfo;
}
}

View File

@ -0,0 +1,90 @@
<?php namespace RVR\Controller;
use DateTime;
use RVR\PersistentData\Model\OAuthSession;
use RVR\PersistentData\Model\User;
use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Response\Redirect;
use SokoWeb\Response\HtmlContent;
class OAuthSessionController implements IAuthenticationRequired
{
private OAuthClientRepository $oAuthClientRepository;
public function __construct()
{
$this->oAuthClientRepository = new OAuthClientRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function auth(): HtmlContent|Redirect
{
$redirectUri = \Container::$request->query('redirect_uri');
$clientId = \Container::$request->query('client_id');
$scope = \Container::$request->query('scope') ? \Container::$request->query('scope'): '';
$state = \Container::$request->query('state');
$nonce = \Container::$request->query('nonce') ? \Container::$request->query('nonce'): '';
$codeChallenge = \Container::$request->query('code_challenge') ?: null;
$codeChallengeMethod = \Container::$request->query('code_challenge_method') ?: null;
if (!$clientId || !$redirectUri || !$state || (!$codeChallenge && $codeChallengeMethod)) {
return new HtmlContent('oauth/oauth_error', ['error' => 'An invalid request was made. Please start authentication again.']);
}
if ($codeChallenge && (strlen($codeChallenge) < 43 || strlen($codeChallenge) > 128)) {
return new HtmlContent('oauth/oauth_error', ['error' => 'Code challenge should be one between 43 and 128 characters long.']);
}
$possibleCodeChallengeMethods = ['plain', 'S256'];
if ($codeChallenge && !in_array($codeChallengeMethod, $possibleCodeChallengeMethods)) {
return new HtmlContent('oauth/oauth_error', ['error' => 'Code challenge method should be one of the following: ' . implode(',', $possibleCodeChallengeMethods)]);
}
$client = $this->oAuthClientRepository->getByClientId($clientId);
if ($client === null) {
return new HtmlContent('oauth/oauth_error', ['error' => 'Client is not found.']);
}
$redirectUriQueryParsed = [];
if (str_contains('?', $redirectUri)) {
[$redirectUriBase, $redirectUriQuery] = explode('?', $redirectUri, 2);
parse_str($redirectUriQuery, $redirectUriQueryParsed);
} else {
$redirectUriBase = $redirectUri;
}
if (!in_array($redirectUriBase, $client->getRedirectUrisArray())) {
return new HtmlContent('oauth/oauth_error', ['error' => 'Redirect URI \'' . $redirectUriBase .'\' is not allowed for this client.']);
}
/**
* @var ?User $user
*/
$user = \Container::$request->user();
$code = bin2hex(random_bytes(16));
$session = new OAuthSession();
$session->setClientId($clientId);
$session->setNonce($nonce);
$session->setScope($scope);
$session->setCodeChallenge($codeChallenge);
$session->setCodeChallengeMethod($codeChallengeMethod);
$session->setUser($user);
$session->setCode($code);
$session->setCreatedDate(new DateTime());
$session->setExpiresDate(new DateTime('+5 minutes'));
\Container::$persistentDataManager->saveToDb($session);
$redirectUriQueryParsed = array_merge($redirectUriQueryParsed, [
'state' => $state,
'code' => $code
]);
$finalRedirectUri = $redirectUriBase . '?' . http_build_query($redirectUriQueryParsed);
return new Redirect($finalRedirectUri, IRedirect::TEMPORARY);
}
}

View File

@ -0,0 +1,239 @@
<?php namespace RVR\Controller;
use Container;
use DateTime;
use RVR\Finance\ExchangeRateCalculator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\Transaction;
use RVR\PersistentData\Model\TransactionPayee;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\CommunityRepository;
use RVR\Repository\CurrencyRepository;
use RVR\Repository\TransactionRepository;
use RVR\Repository\TransactionPayeeRepository;
use RVR\Repository\EventRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
class TransactionController implements IAuthenticationRequired, ISecured
{
private CommunityRepository $communityRepository;
private CommunityMemberRepository $communityMemberRepository;
private CurrencyRepository $currencyRepository;
private TransactionRepository $transactionRepository;
private TransactionPayeeRepository $transactionPayeeRepository;
private EventRepository $eventRepository;
private ?Community $community;
private ?CommunityMember $ownCommunityMember;
public function __construct()
{
$this->communityRepository = new CommunityRepository();
$this->communityMemberRepository = new CommunityMemberRepository();
$this->currencyRepository = new CurrencyRepository();
$this->transactionRepository = new TransactionRepository();
$this->transactionPayeeRepository = new TransactionPayeeRepository();
$this->eventRepository = new EventRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function authorize(): bool
{
$communitySlug = \Container::$request->query('communitySlug');
$this->community = $this->communityRepository->getBySlug($communitySlug);
if ($this->community === null) {
return false;
}
/**
* @var User $user
*/
$user = \Container::$request->user();
$this->ownCommunityMember = $this->communityMemberRepository->getByCommunityAndUser($this->community, $user);
if ($this->ownCommunityMember === null) {
return false;
}
return true;
}
public function getTransactions(): IContent
{
Container::$persistentDataManager->loadRelationsFromDb($this->community, true, ['main_currency']);
$exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
$eventSlug = Container::$request->query('event');
if ($eventSlug) {
$event = $this->eventRepository->getBySlug($eventSlug);
} else {
$event = null;
}
$itemsPerPage = 50;
$numberOfTransactions = $event ?
$this->transactionRepository->countAllByEvent($event) :
$this->transactionRepository->countAllByCommunity($this->community);
$currentPage = Container::$request->query('page') ?: 1;
$transactions = $event ?
$this->transactionRepository->getPagedByEvent(
$event,
$currentPage,
$itemsPerPage,
true,
['currency', 'payer_user']
) :
$this->transactionRepository->getPagedByCommunity(
$this->community,
$currentPage,
$itemsPerPage,
true,
['event', 'currency', 'payer_user']
);
$transactions = iterator_to_array($transactions);
Container::$persistentDataManager->loadMultiRelationsFromDb($transactions, 'payees', true, ['user']);
return new HtmlContent('communities/transactions', [
'community' => $this->community,
'event' => $event,
'exchangeRateCalculator' => $exchangeRateCalculator,
'pages' => ceil($numberOfTransactions / $itemsPerPage),
'currentPage' => $currentPage,
'numberOfTransactions' => $numberOfTransactions,
'transactions' => $transactions,
'members' => $this->getMembers($this->community)
]);
}
public function getTransactionEdit(): ?IContent
{
$transactionId = Container::$request->query('transactionId');
if ($transactionId) {
$transaction = $this->transactionRepository->getById($transactionId);
if ($transaction === null) {
return null;
}
Container::$persistentDataManager->loadRelationsFromDb($transaction, false, ['event']);
$event = $transaction->getEvent();
$payeeUserIds = [];
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
$payeeUserIds[] = $payee->getUserId();
}
} else {
$transaction = null;
$eventSlug = Container::$request->query('event');
if ($eventSlug) {
$event = $this->eventRepository->getBySlug($eventSlug);
} else {
$event = null;
}
$payeeUserIds = [];
}
return new HtmlContent('communities/transaction_edit', [
'community' => $this->community,
'transaction' => $transaction,
'payeeUserIds' => $payeeUserIds,
'event' => $event,
'members' => $this->getMembers($this->community),
'currencies' => $this->getCurrencies($this->community)
]);
}
public function saveTransaction(): ?IContent
{
$transactionId = Container::$request->query('transactionId');
if ($transactionId) {
$transaction = $this->transactionRepository->getById($transactionId);
} else {
$transaction = new Transaction();
$transaction->setCommunity($this->community);
}
$transaction->setEventId(Container::$request->post('event_id') ?: null);
$transaction->setCurrencyId(Container::$request->post('currency_id'));
$transaction->setPayerUserId(Container::$request->post('payer_user_id'));
$transaction->setDescription(Container::$request->post('description'));
$transaction->setSum(Container::$request->post('sum'));
$transaction->setTimeDate(new DateTime(Container::$request->post('time')));
Container::$persistentDataManager->saveToDb($transaction);
$payeeUserIds = array_unique(Container::$request->post('payee_user_ids'));
if (count($payeeUserIds) === $this->communityMemberRepository->countAllByCommunity($this->community)) {
$payeeUserIds = [];
}
$currentPayees = [];
foreach ($payeeUserIds as $payeeUserId) {
$payee = new TransactionPayee();
$payee->setTransaction($transaction);
$payee->setUserId((int)$payeeUserId);
$currentPayees[(int)$payeeUserId] = $payee;
}
$existingPayees = [];
if ($transactionId) {
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
$existingPayees[$payee->getUserId()] = $payee;
}
}
foreach (array_diff_key($currentPayees, $existingPayees) as $newPayee) {
Container::$persistentDataManager->saveToDb($newPayee);
}
foreach (array_diff_key($existingPayees, $currentPayees) as $deletedPayee) {
Container::$persistentDataManager->deleteFromDb($deletedPayee);
}
return new JsonContent(['success' => true]);
}
public function deleteTransaction(): IContent
{
$transaction = $this->transactionRepository->getById(Container::$request->query('transactionId'));
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
Container::$persistentDataManager->deleteFromDb($payee);
}
Container::$persistentDataManager->deleteFromDb($transaction);
return new JsonContent(['success' => true]);
}
private function getMembers(Community $community): array
{
$members = iterator_to_array($this->communityMemberRepository->getAllByCommunity($community, true, ['user']));
usort($members, function ($a, $b) {
return strnatcmp($a->getUser()->getDisplayName(), $b->getUser()->getDisplayName());
});
return $members;
}
private function getCurrencies(Community $community): array
{
$currencies = iterator_to_array($this->currencyRepository->getAllByCommunity($community));
usort($currencies, function ($a, $b) {
return strnatcmp($a->getCode(), $b->getCode());
});
usort($currencies, function ($a, $b) use ($community) {
return (int)($b->getId() === $community->getMainCurrencyId()) - (int)($a->getId() === $community->getMainCurrencyId());
});
return $currencies;
}
}

View File

@ -2,33 +2,29 @@
use DateTime; use DateTime;
use SokoWeb\Http\Request; use SokoWeb\Http\Request;
use SokoWeb\Interfaces\Authorization\ISecured; use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect; use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\OAuth\GoogleOAuth; use SokoWeb\OAuth\GoogleOAuth;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use SokoWeb\Response\HtmlContent; use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent; use SokoWeb\Response\JsonContent;
use SokoWeb\Response\Redirect; use SokoWeb\Response\Redirect;
use SokoWeb\Util\JwtParser; use SokoWeb\Util\JwtParser;
use RVR\Repository\UserRepository;
class UserController implements ISecured class UserController implements IAuthenticationRequired
{ {
private IRequest $request; private UserRepository $userRepository;
private PersistentDataManager $pdm; public function __construct()
public function __construct(IRequest $request)
{ {
$this->request = $request; $this->userRepository = new UserRepository();
$this->pdm = new PersistentDataManager();
} }
public function authorize(): bool public function isAuthenticationRequired(): bool
{ {
return $this->request->user() !== null; return true;
} }
public function getAccount(): IContent public function getAccount(): IContent
@ -36,29 +32,153 @@ class UserController implements ISecured
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
return new HtmlContent('account/account', ['user' => $user->toArray()]); return new HtmlContent('account/account', ['user' => $user->toArray()]);
} }
public function getGoogleConnectRedirect(): IRedirect
{
/**
* @var User $user
*/
$user = \Container::$request->user();
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
\Container::$request->session()->set('oauth_state', $state);
\Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleConnect-confirm')->generateLink(),
$nonce,
$user->getEmail()
);
return new Redirect($url, IRedirect::TEMPORARY);
}
public function getGoogleConnectConfirm(): IContent
{
$defaultError = 'Authentication with Google failed. Please <a href="' . \Container::$routeCollection->getRoute('account.googleConnect')->generateLink() . '" title="Connect with Google">try again</a>!';
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
\Container::$request->query('code'),
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleConnect-confirm')->generateLink()
);
if (!isset($tokenData['id_token'])) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
$jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
$anotherUser = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($anotherUser !== null) {
return new HtmlContent('account/google_connect', [
'success' => false,
'error' => 'This Google account is linked to another account.'
]);
}
\Container::$request->session()->set('google_user_data', $idToken);
/**
* @var User $user
*/
$user = \Container::$request->user();
return new HtmlContent('account/google_connect', [
'success' => true,
'googleAccount' => $idToken['email'],
'userEmail' => $user->getEmail()
]);
}
public function connectGoogle(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong.'
]
]);
}
$googleUserData = \Container::$request->session()->get('google_user_data');
$user->setGoogleSub($googleUserData['sub']);
\Container::$persistentDataManager->saveToDb($user);
return new JsonContent(['success' => true]);
}
public function getGoogleDisconnectConfirm(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
return new HtmlContent('account/google_disconnect', [
'success' => true,
'userEmail' => $user->getEmail()
]);
}
public function disconnectGoogle(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong.'
]
]);
}
$user->setGoogleSub(null);
\Container::$persistentDataManager->saveToDb($user);
return new JsonContent(['success' => true]);
}
public function getGoogleAuthenticateRedirect(): IRedirect public function getGoogleAuthenticateRedirect(): IRedirect
{ {
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16)); $nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state); \Container::$request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce); \Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl( $url = $oAuth->getDialogUrl(
$state, $state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(), \Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
$nonce, $nonce,
$user->getEmail() $user->getEmail()
); );
@ -71,16 +191,16 @@ class UserController implements ISecured
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
return new HtmlContent('account/google_authenticate', ['success' => false]); return new HtmlContent('account/google_authenticate', ['success' => false]);
} }
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken( $tokenData = $oAuth->getToken(
$this->request->query('code'), \Container::$request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink() \Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
); );
if (!isset($tokenData['id_token'])) { if (!isset($tokenData['id_token'])) {
@ -90,7 +210,7 @@ class UserController implements ISecured
$jwtParser = new JwtParser($tokenData['id_token']); $jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload(); $idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) { if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_authenticate', ['success' => false]); return new HtmlContent('account/google_authenticate', ['success' => false]);
} }
@ -102,7 +222,7 @@ class UserController implements ISecured
} }
$authenticatedWithGoogleUntil = new DateTime('+45 seconds'); $authenticatedWithGoogleUntil = new DateTime('+45 seconds');
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil); \Container::$request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
return new HtmlContent('account/google_authenticate', [ return new HtmlContent('account/google_authenticate', [
'success' => true, 'success' => true,
@ -115,19 +235,50 @@ class UserController implements ISecured
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
if (!$this->confirmUserIdentity( if (!$this->confirmUserIdentity(
$user, $user,
$this->request->session()->get('authenticated_with_google_until'), \Container::$request->session()->get('authenticated_with_google_until'),
$this->request->post('password'), \Container::$request->post('password'),
$error $error
)) { )) {
return new JsonContent(['error' => ['errorText' => $error]]); return new JsonContent(['error' => ['errorText' => $error]]);
} }
if (strlen($this->request->post('password_new')) > 0) { $newEmail = \Container::$request->post('email');
if (strlen($this->request->post('password_new')) < 6) { if ($newEmail !== $user->getEmail()) {
if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) {
return new JsonContent(['error' => ['errorText' => 'Please provide a valid email address.']]);
}
if ($this->userRepository->getByEmail($newEmail) !== null) {
return new JsonContent(['error' => ['errorText' => 'The given email address belongs to another account.']]);
}
$user->setEmail($newEmail);
}
$newUsername = \Container::$request->post('username');
if ($newUsername !== $user->getUsername()) {
if (strlen($newUsername) > 0) {
if (filter_var($newUsername, FILTER_VALIDATE_EMAIL)) {
return new JsonContent(['error' => ['errorText' => 'Please select a username that is not a valid email address.']]);
}
if ($this->userRepository->getByUsername($newUsername) !== null) {
return new JsonContent(['error' => ['errorText' => 'The given username is already taken.']]);
}
$user->setUsername($newUsername);
} else {
$user->setUsername(null);
}
}
$newPassword = \Container::$request->post('password_new');
if (strlen($newPassword) > 0) {
if (strlen($newPassword) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
@ -135,7 +286,7 @@ class UserController implements ISecured
]); ]);
} }
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) { if ($newPassword !== \Container::$request->post('password_new_confirm')) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given new passwords do not match.' 'errorText' => 'The given new passwords do not match.'
@ -143,12 +294,16 @@ class UserController implements ISecured
]); ]);
} }
$user->setPlainPassword($this->request->post('password_new')); $user->setPlainPassword($newPassword);
} }
$this->pdm->saveToDb($user); $user->setNickname(\Container::$request->post('nickname'));
$user->setPhone(\Container::$request->post('phone'));
$user->setIdNumber(\Container::$request->post('id_number'));
$this->request->session()->delete('authenticated_with_google_until'); \Container::$persistentDataManager->saveToDb($user);
\Container::$request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }

View File

@ -0,0 +1,38 @@
<?php namespace RVR\Controller;
use RVR\Repository\UserRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent;
class UserSearchController implements IAuthenticationRequired
{
private UserRepository $userRepository;
public function __construct()
{
$this->userRepository = new UserRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function searchUser(): IContent
{
$users = iterator_to_array($this->userRepository->searchByName(\Container::$request->query('q')));
usort($users, function($a, $b) {
return strnatcmp($a->getDisplayName(), $b->getDisplayName());
});
$results = [];
foreach ($users as $user) {
$results[] = ['value' => $user->getId(), 'label' => $user->getFullDisplayName()];
}
return new JsonContent([
'results' => $results
]);
}
}

View File

@ -0,0 +1,18 @@
<?php namespace RVR\Database;
use SokoWeb\Database\AuditLoggerBase;
class AuditLogger extends AuditLoggerBase
{
protected function getModifierId()
{
if (\Container::$request === null) {
return null;
}
$user = \Container::$request->user();
if ($user === null) {
return null;
}
return $user->getUniqueId();
}
}

View File

@ -0,0 +1,136 @@
<?php namespace RVR\Finance;
use Container;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\TransactionRepository;
class BalanceCalculator
{
private Community $community;
private User $user;
private ?Event $event;
private TransactionRepository $transactionRepository;
private CommunityMemberRepository $communityMemberRepository;
private ExchangeRateCalculator $exchangeRateCalculator;
private array $members;
private array $payments;
private array $actualDebts;
public function __construct(Community $community, User $user, ?Event $event = null)
{
$this->community = $community;
$this->user = $user;
$this->event = $event;
$this->transactionRepository = new TransactionRepository();
$this->communityMemberRepository = new CommunityMemberRepository();
$this->exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
}
public function calculate(): array
{
$this->collectMembers();
$this->createPaymentsMatrix();
$this->sumTransactions();
$this->calculateActualDebts();
return $this->calculateBalanceForUser();
}
private function collectMembers(): void
{
$this->members = [];
foreach ($this->communityMemberRepository->getAllByCommunity($this->community, true, ['user']) as $member) {
$this->members[$member->getUserId()] = $member;
}
}
private function createPaymentsMatrix(): void
{
$this->payments = [];
foreach ($this->members as $payerUserId => $member) {
$this->payments[$payerUserId] = [];
foreach ($this->members as $payeeUserId => $member) {
$this->payments[$payerUserId][$payeeUserId] = 0;
}
}
}
private function sumTransactions(): void
{
$membersCount = count($this->members);
if ($this->event !== null) {
$transactions = iterator_to_array($this->transactionRepository->getAllByEvent($this->event, true, ['currency']));
} else {
$transactions = iterator_to_array($this->transactionRepository->getAllByCommunity($this->community, true, ['currency']));
}
Container::$persistentDataManager->loadMultiRelationsFromDb($transactions, 'payees');
foreach ($transactions as $transaction) {
$sum = $this->exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate());
$payees = $transaction->getPayees();
$payeeCount = count($payees);
if ($payeeCount > 0) {
foreach ($payees as $payee) {
$this->payments[$transaction->getPayerUserId()][$payee->getUserId()] += $sum / $payeeCount;
}
} else {
foreach ($this->members as $payeeUserId => $member) {
$this->payments[$transaction->getPayerUserId()][$payeeUserId] += $sum / $membersCount;
}
}
}
}
private function calculateActualDebts(): void
{
$this->actualDebts = [];
foreach ($this->payments as $payerUserId => $paymentsOfPayer) {
foreach ($paymentsOfPayer as $payeeUserId => $sum) {
$actualDebt = $this->payments[$payeeUserId][$payerUserId] - $sum;
if (round($actualDebt, $this->community->getMainCurrency()->getRoundDigits()) > 0.0) {
$this->actualDebts[] = ['payer' => $this->members[$payerUserId], 'payee' => $this->members[$payeeUserId], 'amount' => $actualDebt];
}
}
}
}
private function calculateBalanceForUser(): array
{
$debtItems = [];
$debtBalance = 0.0;
$outstandingItems = [];
$outstandingBalance = 0.0;
foreach ($this->actualDebts as $debt) {
if ($debt['payer']->getId() === $this->user->getId()) {
$debtBalance += $debt['amount'];
$debtItems[] = $debt;
}
if ($debt['payee']->getId() === $this->user->getId()) {
$outstandingBalance += $debt['amount'];
$outstandingItems[] = $debt;
}
}
$absoluteBalance = $outstandingBalance - $debtBalance;
return [
'absoluteBalance' => $absoluteBalance,
'debtItems' => $debtItems,
'debtBalance' => $debtBalance,
'outstandingItems' => $outstandingItems,
'outstandingBalance' => $outstandingBalance
];
}
}

View File

@ -0,0 +1,52 @@
<?php namespace RVR\Finance;
use DateTime;
use RVR\PersistentData\Model\Currency;
use RVR\PersistentData\Model\CurrencyExchangeRate;
use RVR\Repository\CurrencyExchangeRateRepository;
class ExchangeRateCalculator
{
private Currency $mainCurrency;
private CurrencyExchangeRateRepository $currencyExchangeRateRepository;
private array $exchangeRates = [];
public function __construct(Currency $mainCurrency)
{
$this->mainCurrency = $mainCurrency;
$this->currencyExchangeRateRepository = new CurrencyExchangeRateRepository();
}
public function calculate(float $sumInCurrency, Currency $currency, DateTime $time): float
{
if ($currency->getId() === $this->mainCurrency->getId()) {
return $sumInCurrency;
}
$currentExchangeRate = $this->getCurrentExchangeRate($currency, $time);
if ($currentExchangeRate === null) {
return 0.0;
}
return $sumInCurrency * $currentExchangeRate->getExchangeRate();
}
private function getCurrentExchangeRate(Currency $currency, DateTime $time): ?CurrencyExchangeRate
{
if (!isset($this->exchangeRates[$currency->getId()])) {
$this->exchangeRates[$currency->getId()] = iterator_to_array($this->currencyExchangeRateRepository->getAllByCurrency($currency));
}
$currentExchangeRate = null;
foreach ($this->exchangeRates[$currency->getId()] as $exchangeRate) {
if ($exchangeRate->getValidFromDate() > $time) {
break;
}
$currentExchangeRate = $exchangeRate;
}
return $currentExchangeRate;
}
}

View File

@ -0,0 +1,85 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\ModelWithSlug;
class Community extends ModelWithSlug
{
protected static string $table = 'communities';
protected static array $fields = ['name', 'currency', 'main_currency_id', 'created'];
protected static array $relations = ['main_currency' => Currency::class];
protected static string $slugSource = 'name';
private string $name = '';
private string $currency = '';
private ?Currency $mainCurrency = null;
private ?int $mainCurrencyId = null;
private DateTime $created;
public function setName(string $name): void
{
$this->name = $name;
}
public function setCurrency(string $currency): void
{
$this->currency = $currency;
}
public function setMainCurrency(?Currency $mainCurrency): void
{
$this->mainCurrency = $mainCurrency;
}
public function setMainCurrencyId(?int $mainCurrencyId): void
{
$this->mainCurrencyId = $mainCurrencyId;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getName(): string
{
return $this->name;
}
public function getCurrency(): string
{
return $this->currency;
}
public function getMainCurrency(): ?Currency
{
return $this->mainCurrency;
}
public function getMainCurrencyId(): ?int
{
return $this->mainCurrencyId;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
}

View File

@ -0,0 +1,72 @@
<?php namespace RVR\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class CommunityMember extends Model
{
protected static string $table = 'community_members';
protected static array $fields = ['community_id', 'user_id', 'owner'];
protected static array $relations = ['community' => Community::class, 'user' => User::class];
private ?Community $community = null;
private ?int $communityId = null;
private ?User $user = null;
private ?int $userId = null;
private bool $owner = false;
public function setCommunity(Community $community): void
{
$this->community = $community;
}
public function setCommunityId(int $communityId): void
{
$this->communityId = $communityId;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setOwner(bool $owner): void
{
$this->owner = $owner;
}
public function getCommunity(): ?Community
{
return $this->community;
}
public function getCommunityId(): ?int
{
return $this->communityId;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getOwner(): bool
{
return $this->owner;
}
}

View File

@ -0,0 +1,60 @@
<?php namespace RVR\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class Currency extends Model
{
protected static string $table = 'currencies';
protected static array $fields = ['community_id', 'code', 'round_digits'];
protected static array $relations = ['community' => Community::class];
private ?Community $community = null;
private ?int $communityId = null;
private string $code = '';
private int $roundDigits = 0;
public function setCommunity(Community $community): void
{
$this->community = $community;
}
public function setCommunityId(int $communityId): void
{
$this->communityId = $communityId;
}
public function setCode(string $code): void
{
$this->code = $code;
}
public function setRoundDigits(int $roundDigits): void
{
$this->roundDigits = $roundDigits;
}
public function getCommunity(): ?Community
{
return $this->community;
}
public function getCommunityId(): ?int
{
return $this->communityId;
}
public function getCode(): string
{
return $this->code;
}
public function getRoundDigits(): int
{
return $this->roundDigits;
}
}

View File

@ -0,0 +1,71 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class CurrencyExchangeRate extends Model
{
protected static string $table = 'currency_exchange_rates';
protected static array $fields = ['currency_id', 'exchange_rate', 'valid_from'];
protected static array $relations = ['currency' => Currency::class];
private ?Currency $currency = null;
private ?int $currencyId = null;
private float $exchangeRate = 0.0;
private DateTime $validFrom;
public function setCurrency(Currency $currency): void
{
$this->currency = $currency;
}
public function setCurrencyId(int $currencyId): void
{
$this->currencyId = $currencyId;
}
public function setExchangeRate(float $exchangeRate): void
{
$this->exchangeRate = $exchangeRate;
}
public function setValidFromDate(DateTime $validFrom): void
{
$this->validFrom = $validFrom;
}
public function setValidFrom(string $validFrom): void
{
$this->validFrom = new DateTime($validFrom);
}
public function getCurrency(): ?Currency
{
return $this->currency;
}
public function getCurrencyId(): ?int
{
return $this->currencyId;
}
public function getExchangeRate(): float
{
return $this->exchangeRate;
}
public function getValidFromDate(): DateTime
{
return $this->validFrom;
}
public function getValidFrom(): string
{
return $this->validFrom->format('Y-m-d H:i:s');
}
}

View File

@ -0,0 +1,107 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\ModelWithSlug;
class Event extends ModelWithSlug
{
protected static string $table = 'events';
protected static array $fields = ['community_id', 'start', 'end', 'title', 'description'];
protected static array $relations = ['community' => Community::class];
protected static string $slugSource = 'title';
private ?Community $community = null;
private int $communityId;
private DateTime $start;
private DateTime $end;
private string $title = '';
private string $description = '';
public function setCommunity(Community $community): void
{
$this->community = $community;
}
public function setCommunityId(int $communityId): void
{
$this->communityId = $communityId;
}
public function setStartDate(DateTime $start): void
{
$this->start = $start;
}
public function setStart(string $start): void
{
$this->start = new DateTime($start);
}
public function setEndDate(DateTime $end): void
{
$this->end = $end;
}
public function setEnd(string $end): void
{
$this->end = new DateTime($end);
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getCommunity(): ?Community
{
return $this->community;
}
public function getCommunityId(): int
{
return $this->communityId;
}
public function getStartDate(): DateTime
{
return $this->start;
}
public function getStart(): string
{
return $this->start->format('Y-m-d H:i:s');
}
public function getEndDate(): DateTime
{
return $this->end;
}
public function getEnd(): string
{
return $this->end->format('Y-m-d H:i:s');
}
public function getTitle(): string
{
return $this->title;
}
public function getDescription(): string
{
return $this->description;
}
}

View File

@ -0,0 +1,91 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class OAuthClient extends Model
{
protected static string $table = 'oauth_clients';
protected static array $fields = ['client_id', 'client_secret', 'redirect_uris', 'preapproved', 'created'];
private string $clientId = '';
private string $clientSecret = '';
private array $redirectUris = [];
private bool $preapproved = false;
private DateTime $created;
public function setClientId(string $clientId): void
{
$this->clientId = $clientId;
}
public function setClientSecret(string $clientSecret): void
{
$this->clientSecret = $clientSecret;
}
public function setRedirectUrisArray(array $redirectUris): void
{
$this->redirectUris = $redirectUris;
}
public function setRedirectUris(string $redirectUris): void
{
$this->redirectUris = json_decode($redirectUris, true);
}
public function setPreapproved(bool $preapproved): void
{
$this->preapproved = $preapproved;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getClientId(): string
{
return $this->clientId;
}
public function getClientSecret(): string
{
return $this->clientSecret;
}
public function getRedirectUrisArray(): array
{
return $this->redirectUris;
}
public function getRedirectUris(): string
{
return json_encode($this->redirectUris);
}
public function getPreapproved(): bool
{
return $this->preapproved;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
}

View File

@ -0,0 +1,182 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class OAuthSession extends Model
{
protected static string $table = 'oauth_sessions';
protected static array $fields = ['client_id', 'scope', 'nonce', 'code_challenge', 'code_challenge_method', 'user_id', 'code', 'created', 'expires', 'token_claimed'];
protected static array $relations = ['user' => User::class];
private static array $possibleScopeValues = ['openid', 'email', 'profile', 'union_profile'];
private static array $possibleCodeChallengeMethodValues = ['plain', 'S256'];
private string $clientId = '';
private array $scope = [];
private string $nonce = '';
private ?string $codeChallenge = null;
private ?string $codeChallengeMethod = null;
private ?User $user = null;
private ?int $userId = null;
private string $code = '';
private DateTime $created;
private DateTime $expires;
private bool $tokenClaimed = false;
public function setScopeArray(array $scope): void
{
$this->scope = array_intersect($scope, self::$possibleScopeValues);
}
public function setClientId(string $clientId): void
{
$this->clientId = $clientId;
}
public function setScope(string $scope): void
{
$this->setScopeArray(explode(' ', $scope));
}
public function setNonce(string $nonce): void
{
$this->nonce = $nonce;
}
public function setCodeChallenge(?string $codeChallenge): void
{
$this->codeChallenge = $codeChallenge;
}
public function setCodeChallengeMethod(?string $codeChallengeMethod): void
{
if ($codeChallengeMethod !== null && !in_array($codeChallengeMethod, self::$possibleCodeChallengeMethodValues)) {
throw new \UnexpectedValueException($codeChallengeMethod . ' is not possible for challengeMethod!');
}
$this->codeChallengeMethod = $codeChallengeMethod;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setCode(string $code): void
{
$this->code = $code;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setExpiresDate(DateTime $expires): void
{
$this->expires = $expires;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function setExpires(string $expires): void
{
$this->expires = new DateTime($expires);
}
public function setTokenClaimed(bool $tokenClaimed): void
{
$this->tokenClaimed = $tokenClaimed;
}
public function getClientId(): string
{
return $this->clientId;
}
public function getScope(): string
{
return implode(' ', $this->scope);
}
public function getScopeArray(): array
{
return $this->scope;
}
public function getNonce(): string
{
return $this->nonce;
}
public function getCodeChallenge(): ?string
{
return $this->codeChallenge;
}
public function getCodeChallengeMethod(): ?string
{
return $this->codeChallengeMethod;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getCode(): string
{
return $this->code;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
public function getExpiresDate(): DateTime
{
return $this->expires;
}
public function getExpires(): string
{
return $this->expires->format('Y-m-d H:i:s');
}
public function getTokenClaimed(): bool
{
return $this->tokenClaimed;
}
}

View File

@ -0,0 +1,81 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class OAuthToken extends Model
{
protected static string $table = 'oauth_tokens';
protected static array $fields = ['session_id', 'created', 'expires'];
protected static array $relations = ['session' => OAuthSession::class];
private ?OAuthSession $session = null;
private ?int $sessionId = null;
private DateTime $created;
private DateTime $expires;
public function setSession(OAuthSession $session): void
{
$this->session = $session;
}
public function setSessionId(int $sessionId): void
{
$this->sessionId = $sessionId;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setExpiresDate(DateTime $expires): void
{
$this->expires = $expires;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function setExpires(string $expires): void
{
$this->expires = new DateTime($expires);
}
public function getSession(): ?OAuthSession
{
return $this->session;
}
public function getSessionId(): ?int
{
return $this->sessionId;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
public function getExpiresDate(): DateTime
{
return $this->expires;
}
public function getExpires(): string
{
return $this->expires->format('Y-m-d H:i:s');
}
}

View File

@ -0,0 +1,201 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class Transaction extends Model
{
protected static string $table = 'transactions';
protected static array $fields = ['community_id', 'event_id', 'currency_id', 'payer_user_id', 'payee_user_id', 'description', 'sum', 'time'];
protected static array $relations = [
'community' => Community::class,
'event' => Event::class,
'currency' => Currency::class,
'payer_user' => User::class,
'payee_user' => User::class
];
protected static array $multiRelations = [
'payees' => [TransactionPayee::class, 'transaction']
];
private ?Community $community = null;
private int $communityId;
private ?Event $event = null;
private ?int $eventId = null;
private ?Currency $currency = null;
private int $currencyId;
private ?User $payerUser = null;
private int $payerUserId;
private ?User $payeeUser = null;
private ?int $payeeUserId = null;
private ?array $payees = null;
private string $description = '';
private float $sum = 0.0;
private DateTime $time;
public function setCommunity(Community $community): void
{
$this->community = $community;
}
public function setCommunityId(int $communityId): void
{
$this->communityId = $communityId;
}
public function setEvent(?Event $event): void
{
$this->event = $event;
}
public function setEventId(?int $eventId): void
{
$this->eventId = $eventId;
}
public function setCurrency(Currency $currency): void
{
$this->currency = $currency;
}
public function setCurrencyId(int $currencyId): void
{
$this->currencyId = $currencyId;
}
public function setPayerUser(User $payerUser): void
{
$this->payerUser = $payerUser;
}
public function setPayerUserId(int $payerUserId): void
{
$this->payerUserId = $payerUserId;
}
public function setPayeeUser(?User $payeeUser): void
{
$this->payeeUser = $payeeUser;
}
public function setPayeeUserId(?int $payeeUserId): void
{
$this->payeeUserId = $payeeUserId;
}
public function setPayees(array $payees): void
{
$this->payees = $payees;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function setSum(float $sum): void
{
$this->sum = $sum;
}
public function setTimeDate(DateTime $time): void
{
$this->time = $time;
}
public function setTime(string $time): void
{
$this->time = new DateTime($time);
}
public function getCommunity(): ?Community
{
return $this->community;
}
public function getCommunityId(): int
{
return $this->communityId;
}
public function getEvent(): ?Event
{
return $this->event;
}
public function getEventId(): ?int
{
return $this->eventId;
}
public function getCurrency(): ?Currency
{
return $this->currency;
}
public function getCurrencyId(): int
{
return $this->currencyId;
}
public function getPayerUser(): ?User
{
return $this->payerUser;
}
public function getPayerUserId(): int
{
return $this->payerUserId;
}
public function getPayeeUser(): ?User
{
return $this->payeeUser;
}
public function getPayeeUserId(): ?int
{
return $this->payeeUserId;
}
public function getPayees(): ?array
{
return $this->payees;
}
public function getDescription(): string
{
return $this->description;
}
public function getSum(): float
{
return $this->sum;
}
public function getTimeDate(): DateTime
{
return $this->time;
}
public function getTime(): string
{
return $this->time->format('Y-m-d H:i:s');
}
}

View File

@ -0,0 +1,60 @@
<?php namespace RVR\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class TransactionPayee extends Model
{
protected static string $table = 'transaction_payees';
protected static array $fields = ['transaction_id', 'user_id'];
protected static array $relations = ['transaction' => Transaction::class, 'user' => User::class];
private ?Transaction $transaction = null;
private ?int $transactionId = null;
private ?User $user = null;
private ?int $userId = null;
public function setTransaction(Transaction $transaction): void
{
$this->transaction = $transaction;
}
public function setTransactionId(int $transactionId): void
{
$this->transactionId = $transactionId;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function getTransaction(): ?Transaction
{
return $this->transaction;
}
public function getTransactionId(): ?int
{
return $this->transactionId;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
}

View File

@ -8,12 +8,14 @@ class User extends Model implements IUser
{ {
protected static string $table = 'users'; protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'google_sub', 'created']; protected static array $fields = ['email', 'username', 'password', 'type', 'google_sub', 'created', 'full_name', 'nickname', 'phone', 'id_number'];
private static array $types = ['user', 'admin']; private static array $types = ['user', 'admin'];
private string $email = ''; private string $email = '';
private ?string $username = null;
private ?string $password = null; private ?string $password = null;
private string $type = 'user'; private string $type = 'user';
@ -22,11 +24,24 @@ class User extends Model implements IUser
private DateTime $created; private DateTime $created;
private string $fullName = '';
private string $nickname = '';
private string $phone = '';
private string $idNumber = '';
public function setEmail(string $email): void public function setEmail(string $email): void
{ {
$this->email = $email; $this->email = $email;
} }
public function setUsername(?string $username): void
{
$this->username = $username;
}
public function setPassword(?string $hashedPassword): void public function setPassword(?string $hashedPassword): void
{ {
$this->password = $hashedPassword; $this->password = $hashedPassword;
@ -59,11 +74,36 @@ class User extends Model implements IUser
$this->created = new DateTime($created); $this->created = new DateTime($created);
} }
public function setFullName(string $fullName): void
{
$this->fullName = $fullName;
}
public function setNickname(string $nickname): void
{
$this->nickname = $nickname;
}
public function setPhone(string $phone): void
{
$this->phone = $phone;
}
public function setIdNumber(string $idNumber): void
{
$this->idNumber = $idNumber;
}
public function getEmail(): string public function getEmail(): string
{ {
return $this->email; return $this->email;
} }
public function getUsername(): ?string
{
return $this->username;
}
public function getPassword(): ?string public function getPassword(): ?string
{ {
return $this->password; return $this->password;
@ -89,6 +129,26 @@ class User extends Model implements IUser
return $this->created->format('Y-m-d H:i:s'); return $this->created->format('Y-m-d H:i:s');
} }
public function getFullName(): string
{
return $this->fullName;
}
public function getNickname(): string
{
return $this->nickname;
}
public function getPhone(): string
{
return $this->phone;
}
public function getIdNumber(): string
{
return $this->idNumber;
}
public function hasPermission(int $permission): bool public function hasPermission(int $permission): bool
{ {
switch ($permission) { switch ($permission) {
@ -108,7 +168,12 @@ class User extends Model implements IUser
public function getDisplayName(): string public function getDisplayName(): string
{ {
return $this->email; return $this->nickname ?: $this->fullName;
}
public function getFullDisplayName(): string
{
return $this->nickname ? $this->nickname . ' (' . $this->fullName . ')' : $this->fullName;
} }
public function checkPassword(string $password): bool public function checkPassword(string $password): bool

View File

@ -0,0 +1,51 @@
<?php namespace RVR\Repository;
use Generator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\User;
use SokoWeb\Database\Query\Select;
class CommunityMemberRepository
{
public function getById(int $id): ?CommunityMember
{
return \Container::$persistentDataManager->selectFromDbById($id, CommunityMember::class);
}
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllByCommunity($community);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, CommunityMember::class, $useRelations, $withRelations);
}
public function countAllByCommunity(Community $community): int
{
return $this->selectAllByCommunity($community)->count();
}
public function getAllByUser(User $user, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, CommunityMember::class, $useRelations, $withRelations);
}
public function getByCommunityAndUser(Community $community, User $user) : ?CommunityMember
{
$select = new Select(\Container::$dbConnection);
$select->where('community_id', '=', $community->getId());
$select->where('user_id', '=', $user->getId());
return \Container::$persistentDataManager->selectFromDb($select, CommunityMember::class);
}
private function selectAllByCommunity(Community $community): Select
{
$select = new Select(\Container::$dbConnection, CommunityMember::getTable());
$select->where('community_id', '=', $community->getId());
return $select;
}
}

View File

@ -0,0 +1,16 @@
<?php namespace RVR\Repository;
use RVR\PersistentData\Model\Community;
class CommunityRepository
{
public function getById(int $id): ?Community
{
return \Container::$persistentDataManager->selectFromDbById($id, Community::class);
}
public function getBySlug(string $slug): ?Community
{
return \Container::$persistentDataManager->selectFromDbBySlug($slug, Community::class);
}
}

View File

@ -0,0 +1,24 @@
<?php namespace RVR\Repository;
use Generator;
use Container;
use RVR\PersistentData\Model\Currency;
use RVR\PersistentData\Model\CurrencyExchangeRate;
use SokoWeb\Database\Query\Select;
class CurrencyExchangeRateRepository
{
public function getById(int $id): ?CurrencyExchangeRate
{
return \Container::$persistentDataManager->selectFromDbById($id, CurrencyExchangeRate::class);
}
public function getAllByCurrency(Currency $currency): Generator
{
$select = new Select(Container::$dbConnection);
$select->where('currency_id', '=', $currency->getId());
$select->orderBy('valid_from');
yield from Container::$persistentDataManager->selectMultipleFromDb($select, CurrencyExchangeRate::class);
}
}

View File

@ -0,0 +1,32 @@
<?php namespace RVR\Repository;
use Generator;
use Container;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\Currency;
use SokoWeb\Database\Query\Select;
class CurrencyRepository
{
public function getById(int $id): ?Currency
{
return \Container::$persistentDataManager->selectFromDbById($id, Currency::class);
}
public function getAllByCommunity(Community $community, bool $useRelations = false): Generator
{
$select = new Select(Container::$dbConnection);
$select->where('community_id', '=', $community->getId());
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Currency::class, $useRelations);
}
public function getByCommunityAndCurrencyCode(Community $community, string $code): ?Currency
{
$select = new Select(Container::$dbConnection);
$select->where('community_id', '=', $community->getId());
$select->where('code', '=', $code);
return Container::$persistentDataManager->selectFromDb($select, Currency::class);
}
}

View File

@ -0,0 +1,119 @@
<?php namespace RVR\Repository;
use Container;
use DateTime;
use DateInterval;
use Generator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\User;
use SokoWeb\Database\Query\Select;
class EventRepository
{
public function getById(int $id): ?Event
{
return Container::$persistentDataManager->selectFromDbById($id, Event::class);
}
public function getBySlug(string $slug): ?Event
{
return \Container::$persistentDataManager->selectFromDbBySlug($slug, Event::class);
}
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function countAllByCommunity(Community $community): int
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
return $select->count();
}
public function getUpcomingAndRecentByCommunity(Community $community, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC');
$select->limit($limit, 0);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function getUpcomingAndRecentByUser(User $user, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByUser($select, $user);
$this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC');
$select->limit($limit, 0);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function getCurrentByUser(User $user, DateTime $from, int $days, bool $useRelations = false, array $withRelations = []): ?Event
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByUser($select, $user);
$this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC');
$select->limit(1, 0);
return Container::$persistentDataManager->selectFromDb($select, Event::class, $useRelations, $withRelations);
}
public function getPagedByCommunity(Community $community, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$select->orderBy('start', 'DESC');
$select->paginate($page, $itemsPerPage);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function searchByTitle(Community $community, string $title): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$select->where('title', 'LIKE', '%' . $title . '%');
$select->orderBy('start', 'DESC');
$select->limit(10);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class);
}
private function selectAllByCommunity(Select $select, Community $community): void
{
$select->where('community_id', '=', $community->getId());
}
private function selectAllByUser(Select $select, User $user): void
{
$select->innerJoin('communities', ['communities', 'id'], '=', ['events', 'community_id']);
$select->innerJoin('community_members', ['communities', 'id'], '=', ['community_members', 'community_id']);
$select->where(['community_members', 'user_id'], '=', $user->getId());
}
private function selectUpcomingAndRecent(Select $select, DateTime $from, int $days): void
{
$select->where(function (Select $select) use ($from, $days) {
$select->where(function (Select $select) use ($from, $days) {
$select->where('start', '<', (clone $from)->add(DateInterval::createFromDateString("$days days"))->format('Y-m-d H:i:s'));
$select->where('end', '>', $from->format('Y-m-d H:i:s'));
});
$select->orWhere(function (Select $select) use ($from, $days) {
$select->where('end', '>', (clone $from)->sub(DateInterval::createFromDateString("$days days"))->format('Y-m-d H:i:s'));
$select->where('start', '<', $from->format('Y-m-d H:i:s'));
});
});
}
}

View File

@ -0,0 +1,20 @@
<?php namespace RVR\Repository;
use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\OAuthClient;
class OAuthClientRepository
{
public function getById(int $id): ?OAuthClient
{
return \Container::$persistentDataManager->selectFromDbById($id, OAuthClient::class);
}
public function getByClientId(string $clientId): ?OAuthClient
{
$select = new Select(\Container::$dbConnection);
$select->where('client_id', '=', $clientId);
return \Container::$persistentDataManager->selectFromDb($select, OAuthClient::class);
}
}

View File

@ -0,0 +1,30 @@
<?php namespace RVR\Repository;
use DateTime;
use Generator;
use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\OAuthSession;
class OAuthSessionRepository
{
public function getById(int $id): ?OAuthSession
{
return \Container::$persistentDataManager->selectFromDbById($id, OAuthSession::class);
}
public function getByCode(string $code): ?OAuthSession
{
$select = new Select(\Container::$dbConnection);
$select->where('code', '=', $code);
return \Container::$persistentDataManager->selectFromDb($select, OAuthSession::class);
}
public function getAllExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthSession::class);
}
}

View File

@ -0,0 +1,42 @@
<?php namespace RVR\Repository;
use DateTime;
use Generator;
use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\OAuthToken;
use RVR\PersistentData\Model\OAuthSession;
class OAuthTokenRepository
{
public function getById(int $id): ?OAuthToken
{
return \Container::$persistentDataManager->selectFromDbById($id, OAuthToken::class);
}
public function getAllBySession(OAuthSession $session, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllBySession($session);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class, $useRelations, $withRelations);
}
public function countAllBySession(OAuthSession $session): int
{
return $this->selectAllBySession($session)->count();
}
public function getAllExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class);
}
private function selectAllBySession(OAuthSession $session): Select
{
$select = new Select(\Container::$dbConnection, OAuthToken::getTable());
$select->where('session_id', '=', $session->getId());
return $select;
}
}

View File

@ -0,0 +1,23 @@
<?php namespace RVR\Repository;
use Container;
use Generator;
use RVR\PersistentData\Model\Transaction;
use RVR\PersistentData\Model\TransactionPayee;
use SokoWeb\Database\Query\Select;
class TransactionPayeeRepository
{
public function getById(int $id): ?TransactionPayee
{
return Container::$persistentDataManager->selectFromDbById($id, TransactionPayee::class);
}
public function getAllByTransaction(Transaction $transaction, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection);
$select->where('transaction_id', '=', $transaction->getId());
yield from Container::$persistentDataManager->selectMultipleFromDb($select, TransactionPayee::class, $useRelations, $withRelations);
}
}

View File

@ -0,0 +1,100 @@
<?php namespace RVR\Repository;
use Container;
use Generator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\Currency;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\Transaction;
use RVR\PersistentData\Model\User;
use SokoWeb\Database\Query\Select;
class TransactionRepository
{
public function getById(int $id): ?Transaction
{
return Container::$persistentDataManager->selectFromDbById($id, Transaction::class);
}
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllByCommunity($community);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
}
public function getAllByEvent(Event $event, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllByEvent($event);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
}
public function countAllByCommunity(Community $community): int
{
return $this->selectAllByCommunity($community)->count();
}
public function countAllByEvent(Event $event): int
{
return $this->selectAllByEvent($event)->count();
}
public function isAnyForUser(User $user): bool
{
$select = new Select(Container::$dbConnection, Transaction::getTable());
$select->where('payer_user_id', '=', $user->getId());
$select->orWhere('payee_user_id', '=', $user->getId());
$select->orWhere('payee_user_id', '=', null);
return $select->count() > 0;
}
public function isAnyCommon(): bool
{
$select = new Select(Container::$dbConnection, Transaction::getTable());
$select->where('payee_user_id', '=', null);
return $select->count() > 0;
}
public function isAnyForCurrency(Currency $currency): bool
{
$select = new Select(Container::$dbConnection, Transaction::getTable());
$select->where('currency_id', '=', $currency->getId());
return $select->count() > 0;
}
public function getPagedByCommunity(Community $community, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllByCommunity($community);
$select->orderBy('time', 'DESC');
$select->paginate($page, $itemsPerPage);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
}
public function getPagedByEvent(Event $event, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllByEvent($event);
$select->orderBy('time', 'DESC');
$select->paginate($page, $itemsPerPage);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
}
private function selectAllByCommunity(Community $community)
{
$select = new Select(Container::$dbConnection, Transaction::getTable());
$select->where('community_id', '=', $community->getId());
return $select;
}
private function selectAllByEvent(Event $event)
{
$select = new Select(Container::$dbConnection, Transaction::getTable());
$select->where('event_id', '=', $event->getId());
return $select;
}
}

View File

@ -5,20 +5,12 @@ use Generator;
use SokoWeb\Database\Query\Select; use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use RVR\PersistentData\Model\UserPasswordResetter; use RVR\PersistentData\Model\UserPasswordResetter;
use SokoWeb\PersistentData\PersistentDataManager;
class UserPasswordResetterRepository class UserPasswordResetterRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userConfirmationId): ?UserPasswordResetter public function getById(int $userConfirmationId): ?UserPasswordResetter
{ {
return $this->pdm->selectFromDbById($userConfirmationId, UserPasswordResetter::class); return \Container::$persistentDataManager->selectFromDbById($userConfirmationId, UserPasswordResetter::class);
} }
public function getByToken(string $token): ?UserPasswordResetter public function getByToken(string $token): ?UserPasswordResetter
@ -26,7 +18,7 @@ class UserPasswordResetterRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token); $select->where('token', '=', $token);
return $this->pdm->selectFromDb($select, UserPasswordResetter::class); return \Container::$persistentDataManager->selectFromDb($select, UserPasswordResetter::class);
} }
public function getByUser(User $user): ?UserPasswordResetter public function getByUser(User $user): ?UserPasswordResetter
@ -34,7 +26,7 @@ class UserPasswordResetterRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId()); $select->where('user_id', '=', $user->getId());
return $this->pdm->selectFromDb($select, UserPasswordResetter::class); return \Container::$persistentDataManager->selectFromDb($select, UserPasswordResetter::class);
} }
public function getAllExpired(): Generator public function getAllExpired(): Generator
@ -42,6 +34,6 @@ class UserPasswordResetterRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s')); $select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, UserPasswordResetter::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserPasswordResetter::class);
} }
} }

View File

@ -1,22 +1,15 @@
<?php namespace RVR\Repository; <?php namespace RVR\Repository;
use Generator;
use SokoWeb\Interfaces\Repository\IUserRepository; use SokoWeb\Interfaces\Repository\IUserRepository;
use SokoWeb\Database\Query\Select; use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use SokoWeb\PersistentData\PersistentDataManager;
class UserRepository implements IUserRepository class UserRepository implements IUserRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userId): ?User public function getById(int $userId): ?User
{ {
return $this->pdm->selectFromDbById($userId, User::class); return \Container::$persistentDataManager->selectFromDbById($userId, User::class);
} }
public function getByEmail(string $email): ?User public function getByEmail(string $email): ?User
@ -24,7 +17,24 @@ class UserRepository implements IUserRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('email', '=', $email); $select->where('email', '=', $email);
return $this->pdm->selectFromDb($select, User::class); return \Container::$persistentDataManager->selectFromDb($select, User::class);
}
public function getByUsername(string $username): ?User
{
$select = new Select(\Container::$dbConnection);
$select->where('username', '=', $username);
return \Container::$persistentDataManager->selectFromDb($select, User::class);
}
public function getByEmailOrUsername(string $emailOrUsername): ?User
{
if (filter_var($emailOrUsername, FILTER_VALIDATE_EMAIL)) {
return $this->getByEmail($emailOrUsername);
}
return $this->getByUsername($emailOrUsername);
} }
public function getByGoogleSub(string $sub): ?User public function getByGoogleSub(string $sub): ?User
@ -32,6 +42,16 @@ class UserRepository implements IUserRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('google_sub', '=', $sub); $select->where('google_sub', '=', $sub);
return $this->pdm->selectFromDb($select, User::class); return \Container::$persistentDataManager->selectFromDb($select, User::class);
}
public function searchByName(string $name): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('full_name', 'LIKE', '%' . $name . '%');
$select->orWhere('nickname', 'LIKE', '%' . $name . '%');
$select->limit(10);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, User::class);
} }
} }

0
tests/.gitkeep Normal file
View File

View File

@ -4,33 +4,64 @@
@section(main) @section(main)
<h2>Account</h2> <h2>Account</h2>
<div class="box"> <div class="box compactBox">
<form id="accountForm" action="/account" method="post" data-observe-inputs="password_new,password_new_confirm"> <form id="accountForm" action="<?= Container::$routeCollection->getRoute('account-action')->generateLink() ?>" method="post" data-reload-on-success="true" data-observe-inputs="email,username,password_new,password_new_confirm,nickname,phone,id_number">
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?> <?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with your password or with Google to modify your account.</p> <p class="justify small">Please confirm your identity with your password or with Google to modify your account.</p>
<div class="inputWithButton"> <div class="inputWithButton">
<input type="password" class="text name="password" placeholder="Current password" required minlength="6" autofocus><!-- <input type="password" class="text" name="password" autocomplete="current-password" required minlength="6" autofocus><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button> --><button id="authenticateWithGoogleButton" class="yellow" type="button"><i class="fa-brands fa-google"></i></button>
</div> </div>
<?php elseif ($user['password'] !== null): ?> <?php elseif ($user['password'] !== null): ?>
<p class="justify small">Please confirm your identity with your password to modify your account.</p> <p class="justify small">Please confirm your identity with your password to modify your account.</p>
<input type="password" class="text big fullWidth" name="password" placeholder="Current password" required minlength="6" autofocus> <input type="password" class="text big fullWidth" name="password" autocomplete="current-password" required minlength="6" autofocus>
<?php elseif ($user['google_sub'] !== null): ?> <?php elseif ($user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with Google to modify your account.</p> <p class="justify small">Please confirm your identity with Google to modify your account.</p>
<div class="inputWithButton"> <div class="inputWithButton">
<input type="text" class="text" name="password" placeholder="Authenticate with Google..." disabled><!-- <input type="text" class="text" name="password" placeholder="Authenticate with Google..." disabled><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button> --><button id="authenticateWithGoogleButton" class="yellow" type="button"><i class="fa-brands fa-google"></i></button>
</div> </div>
<?php endif; ?> <?php endif; ?>
<hr> <hr>
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?> <p class="formLabel">Email address</p>
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled> <input type="email" class="text big fullWidth" name="email" autocomplete="username" value="<?= $user['email'] ?>">
<input type="password" class="text big fullWidth marginTop" name="password_new" placeholder="New password" minlength="6"> <p class="formLabel marginTop">Username</p>
<input type="password" class="text big fullWidth marginTop" name="password_new_confirm" placeholder="New password confirmation" minlength="6"> <input type="text" class="text big fullWidth" name="username" value="<?= $user['username'] ?>">
<p class="formLabel marginTop">New password</p>
<input type="password" class="text big fullWidth" name="password_new" autocomplete="new-password" minlength="6">
<p class="formLabel marginTop">New password confirmation</p>
<input type="password" class="text big fullWidth" name="password_new_confirm" autocomplete="new-password" minlength="6">
<hr>
<p class="formLabel marginTop">Full name</p>
<input type="text" class="text big fullWidth" name="full_name" value="<?= $user['full_name'] ?>" disabled>
<p class="formLabel marginTop marginTop">Nickname</p>
<input type="text" class="text big fullWidth" name="nickname" value="<?= $user['nickname'] ?>">
<p class="formLabel marginTop marginTop">Phone</p>
<input type="text" class="text big fullWidth" name="phone" value="<?= $user['phone'] ?>">
<p class="formLabel marginTop marginTop">ID number</p>
<input type="text" class="text big fullWidth" name="id_number" value="<?= $user['id_number'] ?>">
<p id="accountFormError" class="formError justify marginTop"></p> <p id="accountFormError" class="formError justify marginTop"></p>
<div class="right marginTop"> <div class="right marginTop">
<button type="submit" name="submit" disabled>Save</button> <button type="submit" name="submit_button" disabled><i class="fa-regular fa-floppy-disk"></i> Save</button>
</div>
<hr>
<div class="center">
<?php if ($user['google_sub'] === null): ?>
<a class="button yellow" href="<?= Container::$routeCollection->getRoute('account.googleConnect')->generateLink() ?>" title="Connect with Google"><i class="fa-solid fa-link"></i> Connect with Google</a>
<?php else: ?>
<?php if ($user['password'] !== null): ?>
<a class="button yellow" href="<?= Container::$routeCollection->getRoute('account.googleDisconnect')->generateLink() ?>" title="Disconnect from Google"><i class="fa-solid fa-link-slash"></i> Disconnect from Google</a>
<?php else: ?>
<p class="bold small">Your account does not have a password. Please set a password if you want to disconnect your account from Google.</p>
<?php endif; ?>
<?php endif; ?>
</div> </div>
</form> </form>
</div> </div>
@endsection @endsection
@section(pageScript)
<script>
var googleAuthenticateUrl = '<?= Container::$routeCollection->getRoute('account.googleAuthenticate')->generateLink() ?>';
</script>
@endsection

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