Compare commits

..
198 Commits
Author SHA1 Message Date
bence 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
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #65
2023-07-24 02:12:15 +02:00
bence aed4548c7b fix select to avoid getting all events as upcoming/recent
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 02:11:27 +02:00
bence 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
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #64
2023-07-24 02:08:11 +02:00
bence 7beec510a1 create endpoints to get current event and current event new transaction
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 02:00:59 +02:00
bence 22b849a1c8 Merge pull request 'select current user as payer by default for new transaction' (!63) from feature/default-payer-is-current-user into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #63
2023-07-24 01:31:22 +02:00
bence 492aa9fcef select current user as payer by default for new transaction
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 01:29:53 +02:00
bence b6da70e015 Merge pull request 'fix transaction edit header' (!62) from bugfix/fix-transaction-edit-header into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #62
2023-07-12 00:40:06 +02:00
bence afead7fd61 Merge pull request 'show balance for events' (!61) from feature/show-balance-for-events into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #61
2023-07-12 00:38:17 +02:00
bence 69acbf708b fix transaction edit header
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-12 00:32:55 +02:00
bence 08ebfae030 show balance for events
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-12 00:26:59 +02:00
bence a597839379 Merge pull request 'feature/RVRNEXT-45-show-recent-events-as-well' (!60) from feature/RVRNEXT-45-show-recent-events-as-well into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #60
2023-07-08 14:44:35 +02:00
bence e0fe6a8622 RVRNEXT-45 update soko-web to 0.13.1
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-08 14:42:09 +02:00
bence 5ebce653dc RVRNEXT-45 show recent events as well with upcoming events 2023-07-08 13:52:24 +02:00
bence 61eb2404a2 Merge pull request 'feature/RVRNEXT-43-split-transactions' (!59) from feature/RVRNEXT-43-split-transactions into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #59
2023-06-17 14:59:09 +02:00
bence e199dfccc7 RVRNEXT-43 update soko-web
rvr-nextgen/pipeline/pr-master This commit looks good
2023-06-17 14:35:00 +02:00
bence 1d0a2b09d5 RVRNEXT-43 adapt balance calculations to multiple payees 2023-06-17 13:47:05 +02:00
bence 1886257b63 RVRNEXT-43 adapt views to multiple payees 2023-06-17 13:46:43 +02:00
bence eda1415e86 RVRNEXT-43 implement multiple payees in transaction controller 2023-06-17 13:46:13 +02:00
bence 4e9c7f8c3c RVRNEXT-43 extend community member repository with count 2023-06-17 13:17:29 +02:00
bence eebf4aff00 RVRNEXT-43 add multi relation for transaction 2023-06-17 13:16:53 +02:00
bence 6dae33507f RVRNEXT-43 add repository for transaction payees 2023-06-17 13:15:54 +02:00
bence 96d5aa2219 RVRNEXT-43 add model for transaction payee 2023-06-17 13:14:43 +02:00
bence 6c326b89e2 RVRNEXT-43 add migration for transaction payees 2023-06-17 13:14:33 +02:00
bence 6c821d41b4 Merge pull request 'feature/RVRNEXT-39-delete-communities' (!58) from feature/RVRNEXT-39-delete-communities into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #58
2023-06-16 21:39:45 +02:00
bence ade02f4736 RVRNEXT-39 add endpoint for community delete
rvr-nextgen/pipeline/pr-master This commit looks good
2023-06-16 21:38:27 +02:00
bence bb5b510315 RVRNEXT-39 add delete button for communities 2023-06-16 21:38:27 +02:00
bence 1267cb3d95 RVRNEXT-39 implement community delete 2023-06-16 21:38:26 +02:00
bence c7641b85e7 RVRNEXT-39 make main currency nullable 2023-06-16 21:28:42 +02:00
bence 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
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #57
2023-05-28 22:31:21 +02:00
bence 58d4f982e9 RVRNEXT-31 remove hack for assets of tom-select
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 22:30:00 +02:00
bence ad06cfaa58 RVRNEXT-31 update soko-web 2023-05-28 22:30:00 +02:00
bence aa2e0e9b4a Merge pull request 'feature/RVRNEXT-22-add-pagination-template' (!56) from feature/RVRNEXT-22-add-pagination-template into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #56
2023-05-28 20:46:22 +02:00
bence d1c926541a RVRNEXT-22 use the new repository methods in controllers
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 20:34:07 +02:00
bence 9929e00a3f RVRNEXT-22 use Select::paginate in repositories 2023-05-28 20:34:07 +02:00
bence bf415a2a46 RVRNEXT-22 use pagination template 2023-05-28 20:34:00 +02:00
bence c0bfa5b334 RVRNEXT-22 add pagination template 2023-05-28 20:33:59 +02:00
bence e122248a97 Merge pull request 'fix sql statement for table transactions' (!55) from bugfix/fix-buggy-migration into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #55
2023-05-28 17:03:11 +02:00
bence d2bb1d4936 fix sql statement for table transactions
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 17:01:19 +02:00
bence b8a901b041 Merge pull request 'feature/RVRNEXT-11-events' (!54) from feature/RVRNEXT-11-events into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #54
2023-05-28 16:52:48 +02:00
bence ede5c24a5d RVRNEXT-11 fix archive in pipeline
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 16:48:38 +02:00
bence 0ae7040804 RVRNEXT-11 update soko-web to 0.12.1 2023-05-28 16:48:38 +02:00
bence f370ccba88 RVRNEXT-11 add upcoming events for user 2023-05-28 16:48:38 +02:00
bence a128168e7f RVRNEXT-11 fix select in community members form 2023-05-28 16:48:38 +02:00
bence 3e9c25f6fe RVRNEXT-11 style improvements 2023-05-28 16:48:38 +02:00
bence 352311a82e RVRNEXT-11 add custom title for main layout 2023-05-28 16:48:38 +02:00
bence c97d80a724 RVRNEXT-11 add upcoming events for community 2023-05-28 16:48:38 +02:00
bence 532dbfd8d6 RVRNEXT-11 add event for transactions 2023-05-28 16:48:37 +02:00
bence 49f3a371e5 RVRNEXT-11 add views for events 2023-05-28 16:48:37 +02:00
bence 2bcaf13ee3 RVRNEXT-11 add controller for events 2023-05-28 16:48:37 +02:00
bence 2fc5812eba RVRNEXT-11 add new endpoints for events 2023-05-28 11:29:36 +02:00
bence 63ca6e7557 RVRNEXT-11 add model and repository for events 2023-05-28 11:29:36 +02:00
bence 224cdc0eda RVRNEXT-11 add migrations for events 2023-05-28 11:29:35 +02:00
bence 42da9eb778 Merge pull request 'bugfix/fixes-for-password-reset' (!53) from bugfix/fixes-for-password-reset into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #53
2023-05-26 16:08:50 +02:00
bence d1dee27191 reset grecaptcha in case of error
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-08 18:48:43 +02:00
bence 726b1818d0 fix error message for password reset request 2023-05-08 18:48:12 +02:00
bence 75f0ae7668 Merge pull request 'feature/RVRNEXT-24-use-slugs-for-communities' (!52) from feature/RVRNEXT-24-use-slugs-for-communities into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #52
2023-05-07 01:57:57 +02:00
bence b43ed72040 RVRNEXT-24 replace id-based search to slug-based for communities
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-07 01:54:10 +02:00
bence 5dc4350b20 RVRNEXT-24 modify model Community to have slug 2023-05-07 01:54:10 +02:00
bence 829588150a RVRNEXT-24 add migration for slug for communities 2023-05-07 01:54:10 +02:00
bence 54d901e45e RVRNEXT-24 update soko-web 2023-05-07 01:54:10 +02:00
bence 5e1692efdb Merge pull request 'feature/RVRNEXT-38-use-font-awesome' (!51) from feature/RVRNEXT-38-use-font-awesome into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #51
2023-05-06 21:52:26 +02:00
bence 32733668ff RVRNEXT-38 do not reference authenticateWithGoogleButton whet it does not exist
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 21:50:46 +02:00
bence 3034217ff8 RVRNEXT-38 fix template for password reset
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 21:43:37 +02:00
bence c664b0a4bf RVRNEXT-38 make it possible to show fa-icons in confirmation modals 2023-05-06 21:43:23 +02:00
bence f397354c1e RVRNEXT-38 replace raw character to fa-icon in transactions 2023-05-06 21:43:01 +02:00
bence e3992d9d55 RVRNEXT-38 add fa-icons to buttons 2023-05-06 21:42:43 +02:00
bence 0f7c9aa02f RVRNEXT-38 replace svgs to fa-icons in normal layous 2023-05-06 21:41:46 +02:00
bence b531589c52 RVRNEXT-38 load font-awesome css 2023-05-06 21:40:56 +02:00
bence dc824a0f70 RVRNEXT-38 install font-awesome 2023-05-06 21:40:35 +02:00
bence 1a06326589 Merge pull request 'feature/RVRNEXT-27-add-connect-with-google' (!50) from feature/RVRNEXT-27-add-connect-with-google into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #50
2023-05-06 19:55:16 +02:00
bence 9780aa62cb RVRNEXT-27 delete unused mail templates
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 19:49:20 +02:00
bence cc3cfc15c8 RVRNEXT-27 fix page header in google_login_error 2023-05-06 19:49:05 +02:00
bence a877484d56 RVRNEXT-27 add link to google connect/disconnect to account page 2023-05-06 19:48:54 +02:00
bence da5c2121ba RVRNEXT-27 add views for google connect/disconnect 2023-05-06 19:48:03 +02:00
bence 73c7d8434e RVRNEXT-27 add logic for google connect/disconnect 2023-05-06 19:46:34 +02:00
bence 3f7817fd1f RVRNEXT-27 add new endpoints for google connect/disconnect 2023-05-06 19:46:09 +02:00
bence 361671a362 Merge pull request 'use the DateTime object when comparing dates in ExhcnageRateCalculator' (!49) from bugfix/fix-current-exchange-rate-calculation into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #49
2023-05-06 13:44:09 +02:00
bence b2babcf30f use the DateTime object when comparing dates in ExhcnageRateCalculator
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 13:41:17 +02:00
bence ea579614b3 Merge pull request 'fix margin of delete button on transaction_edit' (!48) from bugfix/layout-fix into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #48
2023-05-06 13:21:33 +02:00
bence d8786e74d5 fix margin of delete button on transaction_edit
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-06 13:13:15 +02:00
bence 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
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #47
2023-05-02 19:54:08 +02:00
bence 7f1177cc55 RVRNEXT-34 swap delete and save in transaction_edit
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 19:53:06 +02:00
bence a941f5a1cd RVRNEXT-34 remove placeholders, use labels instead 2023-05-02 19:51:15 +02:00
bence ba5dfdbe4d RVRNEXT-34 add style class for form label 2023-05-02 19:49:43 +02:00
bence 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
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #46
2023-05-02 13:13:25 +02:00
bence baddc17b33 RVRNEXT-36 add autocomplete values for username and password fields
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 13:12:19 +02:00
bence 8d495c9f37 Merge pull request 'feature/RVRNEXT-33-no-permanent-session' (!45) from feature/RVRNEXT-33-no-permanent-session into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #45
2023-05-02 13:03:32 +02:00
bence cc8f1e1ace RVRNEXT-33 session should be valid for a session
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 12:59:45 +02:00
bence 97e04d785c RVRNEXT-33 adapt to new soko-web interfaces 2023-05-02 12:59:45 +02:00
bence 0c102da1d7 RVRNEXT-33 update soko-web to 0.10 2023-05-02 12:58:18 +02:00
bence 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
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #44
2023-05-02 02:58:35 +02:00
bence cb76cbb9b1 RVRNEXT-28 add further checks before manupulating db
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 02:56:16 +02:00
bence 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
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #43
2023-05-02 02:51:02 +02:00
bence be93c75fe7 RVRNEXT-30 check if member already has associated transaction
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 02:40:27 +02:00
bence 38d18873b8 Merge pull request 'feature/RVRNEXT-30-replace-choices-to-tomselect' (!41) from feature/RVRNEXT-30-replace-choices-to-tomselect into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #41
2023-05-02 02:24:40 +02:00
bence 5053d37388 RVRNEXT-30 initialize TomSelect in community_members.js
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 02:22:57 +02:00
bence c00be9a49d RVRNEXT-30 remove unnecessary css selector 2023-05-02 02:22:35 +02:00
bence 881280fa3e RVRNEXT-30 use tom-select assets in community_member view 2023-05-02 02:22:10 +02:00
bence 40eb8b5335 RVRNEXT-30 fix submit button name 2023-05-02 02:21:49 +02:00
bence 789e9e199f RVRNEXT-30 install tom-select instead of choices.js 2023-05-02 02:20:40 +02:00
bence be521a42ee Merge pull request 'RVRNEXT-26 show confirmation modal before deleting something' (!40) from feature/RVRNEXT-26-confirmation-before-delete into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #40
2023-05-02 01:49:13 +02:00
bence 65424c2944 RVRNEXT-26 show confirmation modal before deleting something
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 01:03:39 +02:00
bence 490e48a87a Merge pull request 'rename routes' (!39) from feature/rename-routes into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #39
2023-05-02 00:06:11 +02:00
bence b958a2f5b3 rename routes
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-02 00:02:38 +02:00
bence 2da0ccb09b Merge pull request 'feature/layout-improvements' (!38) from feature/layout-improvements into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #38
2023-05-01 23:42:30 +02:00
bence 584fcaba39 make community layout clearer
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-01 23:37:30 +02:00
bence b5dc96f98b fix stupid variable names 2023-05-01 23:02:57 +02:00
bence 4dbbb1321c Merge pull request 'feature/RVRNEXT-7-calculate-balance-for-community-members' (!37) from feature/RVRNEXT-7-calculate-balance-for-community-members into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #37
2023-05-01 22:26:03 +02:00
bence 0a7cfdbb8e RVRNEXT-7 show balances on community home page
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-01 22:01:11 +02:00
bence 751c1895ca RVRNEXT-7 calculate balances in CommunityController 2023-05-01 22:00:32 +02:00
bence 3fd43b6e41 RVRNEXT-7 add BalanceCalculator class 2023-05-01 22:00:13 +02:00
bence ae3554c365 RVRNEXT-7 layout fine-tuning 2023-05-01 21:59:55 +02:00
bence 5526a55145 RVRNEXT-7 add link for transactions 2023-05-01 19:32:35 +02:00
bence 3e6514a2e5 Merge pull request 'feature/RVRNEXT-6-transactions' (!36) from feature/RVRNEXT-6-transactions into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #36
2023-05-01 19:28:27 +02:00
bence 8566641459 RVRNEXT-6 add new routes for transactions
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-01 19:24:24 +02:00
bence 14f7a5b9b5 RVRNEXT-6 add views for transactions 2023-05-01 19:23:48 +02:00
bence 6fb951a01e RVRNEXT-6 add transactioncontroller 2023-05-01 19:23:31 +02:00
bence c7c2df89c5 RVRNEXT-6 add ExchangeRateCalculator 2023-05-01 19:23:19 +02:00
bence 67114211ec RVRNEXT-6 add model and repository for transactions 2023-05-01 19:19:39 +02:00
bence f2d99c4f0f RVRNEXT-6 add migration for transaction db structure 2023-05-01 19:19:10 +02:00
bence 2ed01df331 RVRNEXT-6 layout fine-tuning 2023-05-01 19:18:48 +02:00
bence f89bed25da RVRNEXT-6 update soko-web 2023-05-01 19:15:45 +02:00
bence d707c20ab6 Merge pull request 'feature/update-soko-web-to-0.7' (!35) from feature/update-soko-web-to-0.7 into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #35
2023-04-30 21:07:16 +02:00
bence 49d72f1d48 adapt $withRelations usage to soko-web 0.7
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-30 20:25:21 +02:00
bence eabf17cb77 update soko-web to 0.7 2023-04-30 20:24:10 +02:00
bence 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
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #34
2023-04-28 21:44:18 +02:00
bence 49df206ddc RVRNEXT-5 move saveCommunity under getCommunityEdit
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-28 21:20:08 +02:00
bence e0ea18a6d0 RVRNEXT-5 migration for main_currency_id
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-28 21:06:36 +02:00
bence 20a850f011 RVRNEXT-5 make community main currency a general currency 2023-04-28 21:06:18 +02:00
bence 157f530ad5 RVRNEXT-5 add withRelations to CommunityMemberRepository getters 2023-04-28 20:59:14 +02:00
bence 0b261b6272 RVRNEXT-5 fix audit logger for non-web access 2023-04-28 20:58:19 +02:00
bence 882b326aca Merge pull request 'merge new and edit methods in CommunityController' (!33) from feature/unify-saves-in-communitycontroller into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #33
2023-04-26 09:46:09 +02:00
bence 49fe85dcde merge new and edit methods in CommunityController
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-26 09:43:57 +02:00
bence a021aff92c Merge pull request 'feature/RVRNEXT-5-handling-currency-exchange-rates' (!32) from feature/RVRNEXT-5-handling-currency-exchange-rates into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #32
2023-04-25 19:32:11 +02:00
bence 6abe93e3b5 RVRNEXT-5 fine-tune styles
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-25 19:28:42 +02:00
bence 7e84e6b4ba RVRNEXT-5 add/update layout for currency exchange rates 2023-04-25 19:28:42 +02:00
bence 76ed977375 RVRNEXT-5 add logic for handling currency exchange rates 2023-04-25 19:28:42 +02:00
bence fea403fe5a RVRNEXT-5 add new routes for currency exchange rates 2023-04-25 19:28:42 +02:00
bence 1d96fd0cb9 RVRNEXT-5 add model and repository for currency exchange rate 2023-04-25 19:28:42 +02:00
bence 7437e19de5 RVRNEXT-5 add migration for currency exchange rates 2023-04-23 20:50:45 +02:00
bence 47567c9b75 RVRNEXT-5 add links to community main page 2023-04-23 20:46:11 +02:00
bence ef16e70235 Merge pull request 'feature/RVRNEXT-5-handling-currencies' (!31) from feature/RVRNEXT-5-handling-currencies into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #31
2023-04-23 20:21:31 +02:00
bence 91d064947e RVRNEXT-5 formError should be searched in the whole document
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-23 20:20:10 +02:00
bence 9a6e95673e RVRNEXT-5 add formError for multi for layouts 2023-04-23 20:20:10 +02:00
bence d24c3bb1cb RVRNEXT-5 move private methods to the end of CommunityController 2023-04-23 20:20:10 +02:00
bence 2ad637e55f RVRNEXT-5 make prepare-commit-msg accept fixup commits 2023-04-23 20:20:10 +02:00
bence ac09342c9d RVRNEXT-5 fix layouts for communities 2023-04-23 20:20:10 +02:00
bence 3a1054e1df RVRNEXT-5 adjust table style 2023-04-23 20:20:10 +02:00
bence 0f02c4b5be RVRNEXT-5 update CommunityController with currency operations 2023-04-23 20:20:10 +02:00
bence be0a15d02d RVRNEXT-5 extend views with currency edit 2023-04-23 20:09:49 +02:00
bence 12065b0525 RVRNEXT-5 add model and repository for currencies 2023-04-23 20:09:44 +02:00
bence 0fc21cc461 RVRNEXT-5 add migration for currencies 2023-04-23 20:09:44 +02:00
bence 7d63de4403 RVRNEXT-5 add new endpoints for currency handling 2023-04-23 17:36:03 +02:00
bence c520c256ee Merge pull request 'solve community member edit with forms' (!30) from feature/solve-community-member-edit-with-forms into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #30
2023-04-23 15:59:32 +02:00
bence 2f9b97d1a4 solve community member edit with forms
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-23 15:55:31 +02:00
bence ad18c7cb25 Merge pull request 'use IRouteCollection in app container' (!29) from bugfix/fix-app-container into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #29
2023-04-20 00:34:26 +02:00
bence a2fb166b55 use IRouteCollection in app container
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-20 00:33:06 +02:00
bence f0a7b34212 Merge pull request 'feature/update-soko-web' (!28) from feature/update-soko-web into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #28
2023-04-19 23:43:27 +02:00
bence dccb34971b adapt Container usage to new soko-web
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-19 23:38:43 +02:00
bence 36f767ea9b update soko-web version 2023-04-19 23:38:43 +02:00
bence c0739eeddf Merge pull request 'feature/activate-audit-logging' (!27) from feature/activate-audit-logging into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #27
2023-04-19 00:58:44 +02:00
bence cac30c9203 add auditLogger instance to app container
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-18 23:37:55 +02:00
bence 89b0ce768b add db table for audit log 2023-04-18 23:37:55 +02:00
bence 656ce219ef add class AuditLogger 2023-04-18 23:37:55 +02:00
bence fd30911dd3 Merge pull request 'feature/update-to-soko-web-0.5' (!26) from feature/update-to-soko-web-0.5 into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #26
2023-04-18 23:30:33 +02:00
bence b4cfa7aab2 remove unnecessary startTransaction and commit calls
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-18 23:29:04 +02:00
bence 617ae903ae add error view 500 to app config 2023-04-18 23:29:04 +02:00
bence a7997429b7 add view for error 500 2023-04-18 23:26:42 +02:00
bence 4d5e982370 pass dbConnection to HttpResponse 2023-04-18 23:26:42 +02:00
bence 46ea01efe1 update soko-web to 0.5 2023-04-18 23:20:29 +02:00
bence 4161abe098 Merge pull request 'fix path for antiCsrfTokenExceptions' (!25) from bugfix/fix-path-for-csrf-exceptions into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #25
2023-04-16 22:09:15 +02:00
bence 8777d931a4 fix path for antiCsrfTokenExceptions
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 22:07:57 +02:00
bence d2ef9bb18a Merge pull request 'remove unnecessary use in web.php' (!24) from bugfix/remove-unnecessary-use into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #24
2023-04-16 22:07:30 +02:00
bence 3ec7fc11fa remove unnecessary use in web.php
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 22:01:48 +02:00
bence 9fd8453f63 Merge pull request 'feature/adapt-to-new-soko-web' (!23) from feature/adapt-to-new-soko-web into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #23
2023-04-16 21:16:58 +02:00
bence a7d3942d1f use classes at beginning of web.php
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 21:14:56 +02:00
bence 41933ec510 update soko-web to 0.4
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 20:54:49 +02:00
bence e98bb28faf adapt to soko-web 0.4 2023-04-16 20:54:29 +02:00
bence 34eeb10c27 Merge pull request 'feature/start-using-authentication-required-interface' (!22) from feature/start-using-authentication-required-interface into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #22
2023-04-16 17:53:16 +02:00
bence 70e13f70c0 update soko-web to 0.3
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 17:51:29 +02:00
bence a89182b64f split authentication required and secured controllers 2023-04-16 17:51:29 +02:00
bence 45a22c2dd4 Merge pull request 'feature/upgrade-soko-web' (!21) from feature/upgrade-soko-web into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #21
2023-04-16 16:05:51 +02:00
bence c6d33753c8 adapt to new soko-web
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 16:03:38 +02:00
bence b32092f1cd upgrade soko-web to 0.2.1 2023-04-16 16:03:11 +02:00
bence b5cb59d3ee Merge pull request 'replace hardcoded links to generated ones' (!20) from feature/replace-hardcoded-links into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #20
2023-04-16 14:44:00 +02:00
bence 0b8d7b9cda replace hardcoded links to generated ones
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 14:31:10 +02:00
bence 4f4cc43e90 fix home links in LoginController 2023-04-16 14:30:49 +02:00
bence b066ef22b4 Merge pull request 'add adminer to docker compose stack' (!19) from feature/add-adminer into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #19
2023-04-16 14:24:02 +02:00
bence 08767d1d59 add adminer to docker compose stack
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 14:21:19 +02:00
bence 46404a9d7b Merge pull request 'fix syntax error in migration' (!18) from hotfix/fix-migration into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #18
2023-04-16 14:07:00 +02:00
bence 8a4c5403cb fix syntax error in migration
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 14:05:14 +02:00
bence 15a58e8ace Merge pull request 'implement community basics' (!17) from feature/community-basics into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #17
2023-04-16 14:00:25 +02:00
bence d763c4344c implement community basics
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-16 13:57:54 +02:00
bence 4dc08dffc9 Merge pull request 'feature/fix-oauth-audience' (!16) from feature/fix-oauth-audience into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #16
2023-04-12 02:11:53 +02:00
bence 87b811f716 client id can be anything
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-12 02:10:33 +02:00
bence eb4ebb9582 send clientId as aud 2023-04-12 02:05:44 +02:00
bence 6af7813e3d drop audience from oauth_tokens 2023-04-12 02:04:56 +02:00
bence 31ff9b287d Revert "add aud to jwt"
This reverts commit 97780eb079.
2023-04-12 02:03:15 +02:00
98 changed files with 4023 additions and 454 deletions

No files matched your search

Vendored
+2 -2
View File
@@ -35,7 +35,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'
} }
} }
@@ -53,7 +53,7 @@ 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'
} }
} }
+7 -3
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);
+1 -1
View File
@@ -10,7 +10,7 @@
} }
], ],
"require": { "require": {
"esoko/soko-web": "0.1", "esoko/soko-web": "0.13.1",
"firebase/php-jwt": "^6.4" "firebase/php-jwt": "^6.4"
}, },
"require-dev": { "require-dev": {
Generated
+79 -4
View File
@@ -4,17 +4,92 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f2dcf297a4a619bc5edfe7b4fac0836e", "content-hash": "59130fbd82b1c81275666b16adb0c1a1",
"packages": [ "packages": [
{
"name": "cocur/slugify",
"version": "v4.3.0",
"source": {
"type": "git",
"url": "https://github.com/cocur/slugify.git",
"reference": "652234ef5f1be844a2ae1c36ad1b4c88b05160f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cocur/slugify/zipball/652234ef5f1be844a2ae1c36ad1b4c88b05160f9",
"reference": "652234ef5f1be844a2ae1c36ad1b4c88b05160f9",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ~8.0.0 || ~8.1.0 || ~8.2.0"
},
"conflict": {
"symfony/config": "<3.4 || >=4,<4.3",
"symfony/dependency-injection": "<3.4 || >=4,<4.3",
"symfony/http-kernel": "<3.4 || >=4,<4.3",
"twig/twig": "<2.12.1"
},
"require-dev": {
"laravel/framework": "^5.0|^6.0|^7.0|^8.0",
"latte/latte": "~2.2",
"league/container": "^2.2.0",
"mikey179/vfsstream": "~1.6.8",
"mockery/mockery": "^1.3",
"nette/di": "~2.4",
"pimple/pimple": "~1.1",
"plumphp/plum": "~0.1",
"symfony/config": "^3.4 || ^4.3 || ^5.0 || ^6.0",
"symfony/dependency-injection": "^3.4 || ^4.3 || ^5.0 || ^6.0",
"symfony/http-kernel": "^3.4 || ^4.3 || ^5.0 || ^6.0",
"symfony/phpunit-bridge": "^5.4 || ^6.0",
"twig/twig": "^2.12.1 || ~3.0",
"zendframework/zend-modulemanager": "~2.2",
"zendframework/zend-servicemanager": "~2.2",
"zendframework/zend-view": "~2.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Cocur\\Slugify\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Florian Eckerstorfer",
"email": "florian@eckerstorfer.co",
"homepage": "https://florian.ec"
},
{
"name": "Ivo Bathke",
"email": "ivo.bathke@gmail.com"
}
],
"description": "Converts a string into a slug.",
"keywords": [
"slug",
"slugify"
],
"support": {
"issues": "https://github.com/cocur/slugify/issues",
"source": "https://github.com/cocur/slugify/tree/v4.3.0"
},
"time": "2022-12-07T19:48:48+00:00"
},
{ {
"name": "esoko/soko-web", "name": "esoko/soko-web",
"version": "v0.1", "version": "v0.13.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://git.esoko.eu/esoko/soko-web.git", "url": "https://git.esoko.eu/esoko/soko-web.git",
"reference": "a17a88e9d44e42cef3d996fc66b4dd1134b85def" "reference": "8bf495c89b4ce1456da1133adc285003e544dd48"
}, },
"require": { "require": {
"cocur/slugify": "^4.3",
"phpmailer/phpmailer": "^6.8", "phpmailer/phpmailer": "^6.8",
"symfony/console": "^5.4", "symfony/console": "^5.4",
"vlucas/phpdotenv": "^5.5" "vlucas/phpdotenv": "^5.5"
@@ -33,7 +108,7 @@
"GNU GPL 3.0" "GNU GPL 3.0"
], ],
"description": "Lightweight web framework", "description": "Lightweight web framework",
"time": "2023-04-07T17:32:15+00:00" "time": "2023-07-08T12:38:40+00:00"
}, },
{ {
"name": "firebase/php-jwt", "name": "firebase/php-jwt",
@@ -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);
}
@@ -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);
}
@@ -0,0 +1,2 @@
ALTER TABLE `oauth_tokens`
DROP `audience`;
@@ -0,0 +1,2 @@
ALTER TABLE `oauth_clients`
MODIFY `client_id` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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`);
@@ -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;
@@ -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`);
@@ -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;
@@ -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`);
@@ -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;
+6
View File
@@ -19,6 +19,12 @@ services:
MYSQL_DATABASE: 'rvr' MYSQL_DATABASE: 'rvr'
MYSQL_USER: 'rvr' MYSQL_USER: 'rvr'
MYSQL_PASSWORD: 'rvr' MYSQL_PASSWORD: 'rvr'
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:
-9
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>
-18
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>
-55
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', substr($_SERVER['REQUEST_URI'], strlen('/')));
$response = new Redirect(Container::$routeCollection->getRoute('login')->generateLink(), IRedirect::TEMPORARY);
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
return;
}
if ($method === 'post' && !in_array($url, $antiCsrfTokenExceptions) && 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();
+144 -12
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 {
@@ -417,14 +454,108 @@ 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;
} }
div.compactBox {
width: 576px;
}
div.transaction {
display: grid;
grid-template-columns: auto auto;
}
div.gridContainer {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
grid-gap: 10px;
}
div.gridContainer > div {
background-color: #eeeef4;
border-radius: 3px;
padding: 5px 10px;
}
table {
border-collapse: separate;
border-spacing: 0;
}
table.fullWidth {
width: 100%;
}
table th {
font-weight: 700;
}
table th, table td {
padding: 3px 0;
vertical-align: middle;
}
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) {
header h1 span { header h1 span {
display: none; display: none;
@@ -433,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%;
} }
@@ -448,7 +580,7 @@ div.box {
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
} }
div.box { div.compactBox {
width: initial; width: initial;
} }
} }
+5 -6
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();
};
})(); })();
@@ -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();
}
});
})();
@@ -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();
}
});
})();
+50 -3
View File
@@ -49,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 () {
@@ -64,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;
} }
@@ -88,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';
@@ -125,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;
@@ -163,12 +192,18 @@ var RVR = {
for (var i = 0; i < observedInputs.length; i++) { for (var i = 0; i < observedInputs.length; i++) {
var input = form.elements[observedInputs[i]]; var input = form.elements[observedInputs[i]];
if (input.type === 'checkbox') {
if (input.defaultChecked !== input.checked) {
anyChanged = true;
}
} else {
if (input.defaultValue !== input.value) { if (input.defaultValue !== input.value) {
anyChanged = true; anyChanged = true;
} }
} }
}
form.elements.submit.disabled = !anyChanged; form.elements['submit_button'].disabled = !anyChanged;
}, },
observeInputsInForm: function (form, observedInputs) { observeInputsInForm: function (form, observedInputs) {
@@ -191,8 +226,16 @@ var RVR = {
} }
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);
};
} }
}; };
@@ -220,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
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"
}
}
}
}
+3 -1
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"
} }
} }
+1 -1
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
+4 -5
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\OAuthClient; use RVR\PersistentData\Model\OAuthClient;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@@ -14,12 +13,13 @@ class AddOAuthClientCommand extends Command
{ {
$this->setName('oauth:add-client') $this->setName('oauth:add-client')
->setDescription('Adding of OAuth client.') ->setDescription('Adding of OAuth client.')
->addArgument('client-id', InputArgument::OPTIONAL, 'Client ID')
->addArgument('preapproved', InputArgument::OPTIONAL, 'Preapproved'); ->addArgument('preapproved', InputArgument::OPTIONAL, 'Preapproved');
} }
public function execute(InputInterface $input, OutputInterface $output): int public function execute(InputInterface $input, OutputInterface $output): int
{ {
$clientId = bin2hex(random_bytes(8)); $clientId = $input->getArgument('client-id') ? $input->getArgument('client-id') : bin2hex(random_bytes(8));
$clientSecret = bin2hex(random_bytes(20)); $clientSecret = bin2hex(random_bytes(20));
$oAuthClient = new OAuthClient(); $oAuthClient = new OAuthClient();
@@ -27,13 +27,12 @@ class AddOAuthClientCommand extends Command
$oAuthClient->setClientSecret($clientSecret); $oAuthClient->setClientSecret($clientSecret);
$oAuthClient->setCreatedDate(new DateTime()); $oAuthClient->setCreatedDate(new DateTime());
if ($input->hasArgument('preapproved') && $input->getArgument('preapproved')) { if ($input->getArgument('preapproved')) {
$oAuthClient->setPreapproved($input->getArgument('preapproved')); $oAuthClient->setPreapproved($input->getArgument('preapproved'));
} }
try { try {
$pdm = new PersistentDataManager(); \Container::$persistentDataManager->saveToDb($oAuthClient);
$pdm->saveToDb($oAuthClient);
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Adding OAuth client failed!</error>'); $output->writeln('<error>Adding OAuth client failed!</error>');
$output->writeln(''); $output->writeln('');
+1 -3
View File
@@ -1,6 +1,5 @@
<?php namespace RVR\Cli; <?php namespace RVR\Cli;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\Repository\OAuthClientRepository; use RVR\Repository\OAuthClientRepository;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@@ -32,8 +31,7 @@ class AddOAuthRedirectUriCommand extends Command
$oAuthClient->setRedirectUrisArray($redirectUris); $oAuthClient->setRedirectUrisArray($redirectUris);
try { try {
$pdm = new PersistentDataManager(); \Container::$persistentDataManager->saveToDb($oAuthClient);
$pdm->saveToDb($oAuthClient);
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Adding redirect URI failed!</error>'); $output->writeln('<error>Adding redirect URI failed!</error>');
$output->writeln(''); $output->writeln('');
+1 -3
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;
@@ -36,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('');
+1 -5
View File
@@ -4,7 +4,6 @@ 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 Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@@ -12,15 +11,12 @@ use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command class MaintainDatabaseCommand extends Command
{ {
private PersistentDataManager $pdm;
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->pdm = new PersistentDataManager();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
} }
@@ -54,7 +50,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);
} }
} }
+1 -3
View File
@@ -1,6 +1,5 @@
<?php namespace RVR\Cli; <?php namespace RVR\Cli;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\Repository\OAuthClientRepository; use RVR\Repository\OAuthClientRepository;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@@ -32,8 +31,7 @@ class RemoveOAuthRedirectUriCommand extends Command
$oAuthClient->setRedirectUrisArray($redirectUris); $oAuthClient->setRedirectUrisArray($redirectUris);
try { try {
$pdm = new PersistentDataManager(); \Container::$persistentDataManager->saveToDb($oAuthClient);
$pdm->saveToDb($oAuthClient);
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Removing redirect URI failed!</error>'); $output->writeln('<error>Removing redirect URI failed!</error>');
$output->writeln(''); $output->writeln('');
+470
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;
}
}
+193
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;
}
}
@@ -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']);
}
}
+34 -11
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']))
]);
} }
} }
+50 -66
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,35 +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;
private string $redirectUrl; private string $redirectUrl;
public function __construct(IRequest $request) 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 = $this->request->session()->has('redirect_after_login') ? $this->redirectUrl = \Container::$request->session()->has('redirect_after_login') ?
$this->request->session()->get('redirect_after_login') : \Container::$request->session()->get('redirect_after_login') :
\Container::$routeCollection->getRoute('index')->generateLink(); \Container::$routeCollection->getRoute('home')->generateLink();
} }
public function getLoginForm() public function getLoginForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl(); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
return new HtmlContent('login/login', ['redirectUrl' => '/' . $this->redirectUrl]); return new HtmlContent('login/login', ['redirectUrl' => $this->redirectUrl]);
} }
public function getGoogleLoginRedirect(): IRedirect public function getGoogleLoginRedirect(): IRedirect
@@ -55,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
); );
@@ -70,12 +62,12 @@ class LoginController
public function getRequestPasswordResetForm() public function getRequestPasswordResetForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl(); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); 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
@@ -85,12 +77,12 @@ class LoginController
public function getResetPasswordForm() public function getResetPasswordForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl(); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); 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()) {
@@ -99,27 +91,27 @@ 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(), 'redirectUrl' => '/' . $this->redirectUrl]); 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(); $this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
$user = $this->userRepository->getByEmailOrUsername($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 / username 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(); $this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
@@ -127,21 +119,21 @@ class LoginController
public function loginWithGoogle() public function loginWithGoogle()
{ {
$defaultError = 'Authentication with Google failed. Please <a href="/login/google" title="Login with Google">try again</a>!'; $defaultError = 'Authentication with Google failed. Please <a href="' . \Container::$routeCollection->getRoute('login.google')->generateLink() . '" title="Login with Google">try again</a>!';
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl(); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
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('login/google_login_error', ['error' => $defaultError]); return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
} }
$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'])) {
@@ -151,7 +143,7 @@ class LoginController
$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_error', ['error' => $defaultError]); return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
} }
@@ -164,7 +156,7 @@ class LoginController
return new HtmlContent('login/google_login_error', ['error' => 'No user found for this Google account.']); return new HtmlContent('login/google_login_error', ['error' => 'No user found for this Google account.']);
} }
$this->request->setUser($user); \Container::$request->setUser($user);
$this->deleteRedirectUrl(); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
@@ -172,39 +164,39 @@ class LoginController
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(); $this->deleteRedirectUrl();
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . $this->redirectUrl '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->getByEmailOrUsername($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' => [
'errorText' => 'No user found with the given email address / username.' 'errorText' => 'No user found with the given email address.'
] ]
]); ]);
} }
@@ -226,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);
@@ -243,27 +231,27 @@ class LoginController
public function resetPassword(): IContent public function resetPassword(): IContent
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl(); $this->deleteRedirectUrl();
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . $this->redirectUrl '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!'
@@ -271,22 +259,18 @@ 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(); $this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
@@ -299,8 +283,8 @@ 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();
@@ -308,6 +292,6 @@ class LoginController
private function deleteRedirectUrl(): void private function deleteRedirectUrl(): void
{ {
$this->request->session()->delete('redirect_after_login'); \Container::$request->session()->delete('redirect_after_login');
} }
} }
+13 -23
View File
@@ -4,40 +4,32 @@ use DateTime;
use RVR\PersistentData\Model\OAuthToken; use RVR\PersistentData\Model\OAuthToken;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use RVR\Repository\OAuthClientRepository; use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Authorization\ISecured; use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IRedirect; use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Response\Redirect; use SokoWeb\Response\Redirect;
use SokoWeb\PersistentData\PersistentDataManager;
use SokoWeb\Response\HtmlContent; use SokoWeb\Response\HtmlContent;
class OAuthAuthController implements ISecured class OAuthAuthController implements IAuthenticationRequired
{ {
private IRequest $request;
private PersistentDataManager $pdm;
private OAuthClientRepository $oAuthClientRepository; private OAuthClientRepository $oAuthClientRepository;
public function __construct(IRequest $request) public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->oAuthClientRepository = new OAuthClientRepository(); $this->oAuthClientRepository = new OAuthClientRepository();
} }
public function authorize(): bool public function isAuthenticationRequired(): bool
{ {
return $this->request->user() !== null; return true;
} }
public function auth() public function auth()
{ {
$redirectUri = $this->request->query('redirect_uri'); $redirectUri = \Container::$request->query('redirect_uri');
$clientId = $this->request->query('client_id'); $clientId = \Container::$request->query('client_id');
$scope = $this->request->query('scope') ? $this->request->query('scope'): ''; $scope = \Container::$request->query('scope') ? \Container::$request->query('scope'): '';
$state = $this->request->query('state'); $state = \Container::$request->query('state');
$nonce = $this->request->query('nonce') ? $this->request->query('nonce'): ''; $nonce = \Container::$request->query('nonce') ? \Container::$request->query('nonce'): '';
if (!$clientId || !$redirectUri || !$state) { if (!$clientId || !$redirectUri || !$state) {
return new HtmlContent('oauth/oauth_error', ['error' => 'An invalid request was made. Please start authentication again.']); return new HtmlContent('oauth/oauth_error', ['error' => 'An invalid request was made. Please start authentication again.']);
@@ -49,8 +41,7 @@ class OAuthAuthController implements ISecured
} }
$redirectUriParsed = parse_url($redirectUri); $redirectUriParsed = parse_url($redirectUri);
$redirectUriHost = $redirectUriParsed['scheme'] . '://' . $redirectUriParsed['host']; $redirectUriBase = $redirectUriParsed['scheme'] . '://' . $redirectUriParsed['host'] . $redirectUriParsed['path'];
$redirectUriBase = $redirectUriHost . $redirectUriParsed['path'];
$redirectUriQuery = []; $redirectUriQuery = [];
if (isset($redirectUriParsed['query'])) { if (isset($redirectUriParsed['query'])) {
parse_str($redirectUriParsed['query'], $redirectUriQuery); parse_str($redirectUriParsed['query'], $redirectUriQuery);
@@ -63,7 +54,7 @@ class OAuthAuthController implements ISecured
/** /**
* @var ?User $user * @var ?User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
$code = bin2hex(random_bytes(16)); $code = bin2hex(random_bytes(16));
$accessToken = bin2hex(random_bytes(16)); $accessToken = bin2hex(random_bytes(16));
@@ -73,10 +64,9 @@ class OAuthAuthController implements ISecured
$token->setUser($user); $token->setUser($user);
$token->setCode($code); $token->setCode($code);
$token->setAccessToken($accessToken); $token->setAccessToken($accessToken);
$token->setAudience($redirectUriHost);
$token->setCreatedDate(new DateTime()); $token->setCreatedDate(new DateTime());
$token->setExpiresDate(new DateTime('+5 minutes')); $token->setExpiresDate(new DateTime('+5 minutes'));
$this->pdm->saveToDb($token); \Container::$persistentDataManager->saveToDb($token);
$redirectUriQuery = array_merge($redirectUriQuery, [ $redirectUriQuery = array_merge($redirectUriQuery, [
'state' => $state, 'state' => $state,
+12 -18
View File
@@ -6,23 +6,19 @@ use RVR\Repository\OAuthTokenRepository;
use RVR\Repository\UserRepository; use RVR\Repository\UserRepository;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use RVR\Repository\OAuthClientRepository; use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent; use SokoWeb\Response\JsonContent;
class OAuthController class OAuthController
{ {
private IRequest $request;
private OAuthClientRepository $oAuthClientRepository; private OAuthClientRepository $oAuthClientRepository;
private OAuthTokenRepository $oAuthTokenRepository; private OAuthTokenRepository $oAuthTokenRepository;
private UserRepository $userRepository; private UserRepository $userRepository;
public function __construct(IRequest $request) public function __construct()
{ {
$this->request = $request;
$this->oAuthClientRepository = new OAuthClientRepository(); $this->oAuthClientRepository = new OAuthClientRepository();
$this->oAuthTokenRepository = new OAuthTokenRepository(); $this->oAuthTokenRepository = new OAuthTokenRepository();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
@@ -30,9 +26,9 @@ class OAuthController
public function getToken(): ?IContent public function getToken(): ?IContent
{ {
$clientId = $this->request->post('client_id'); $clientId = \Container::$request->post('client_id');
$clientSecret = $this->request->post('client_secret'); $clientSecret = \Container::$request->post('client_secret');
$code = $this->request->post('code'); $code = \Container::$request->post('code');
if (!$clientId || !$clientSecret || !$code) { if (!$clientId || !$clientSecret || !$code) {
return new JsonContent([ return new JsonContent([
@@ -59,7 +55,7 @@ class OAuthController
'iat' => (int)$token->getCreatedDate()->getTimestamp(), 'iat' => (int)$token->getCreatedDate()->getTimestamp(),
'nbf' => (int)$token->getCreatedDate()->getTimestamp(), 'nbf' => (int)$token->getCreatedDate()->getTimestamp(),
'exp' => (int)$token->getExpiresDate()->getTimestamp(), 'exp' => (int)$token->getExpiresDate()->getTimestamp(),
'aud' => $token->getAudience(), 'aud' => $clientId,
'nonce' => $token->getNonce() 'nonce' => $token->getNonce()
], $this->getUserInfoInternal( ], $this->getUserInfoInternal(
$this->userRepository->getById($token->getUserId()), $this->userRepository->getById($token->getUserId()),
@@ -80,16 +76,14 @@ class OAuthController
public function getUserInfo() : IContent public function getUserInfo() : IContent
{ {
//TODO: headers should be set by soko-web $authorization = \Container::$request->header('Authorization');
$headers = getallheaders(); if ($authorization === null) {
if (!isset($headers['Authorization'])) {
return new JsonContent([ return new JsonContent([
'error' => 'No Authorization header was sent.' 'error' => 'No Authorization header was sent.'
]); ]);
} }
$accessToken = substr($headers['Authorization'], strlen('Bearer ')); $accessToken = substr($authorization, strlen('Bearer '));
$token = $this->oAuthTokenRepository->getByAccessToken($accessToken); $token = $this->oAuthTokenRepository->getByAccessToken($accessToken);
if ($token === null || $token->getExpiresDate() < new DateTime()) { if ($token === null || $token->getExpiresDate() < new DateTime()) {
@@ -110,10 +104,10 @@ class OAuthController
{ {
return new JsonContent([ return new JsonContent([
'issuer' => $_ENV['APP_URL'], 'issuer' => $_ENV['APP_URL'],
'authorization_endpoint' => $this->request->getBase() . '/oauth/auth', 'authorization_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.auth')->generateLink(),
'token_endpoint' => $this->request->getBase() . '/oauth/token', 'token_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token')->generateLink(),
'userinfo_endpoint' => $this->request->getBase() . '/oauth/userinfo', 'userinfo_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.userinfo')->generateLink(),
'jwks_uri' => $this->request->getBase() . '/oauth/certs', 'jwks_uri' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.certs')->generateLink(),
'response_types_supported' => 'response_types_supported' =>
[ [
'code', 'code',
+239
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;
}
}
+152 -36
View File
@@ -2,12 +2,10 @@
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;
@@ -15,24 +13,18 @@ use SokoWeb\Response\Redirect;
use SokoWeb\Util\JwtParser; use SokoWeb\Util\JwtParser;
use RVR\Repository\UserRepository; use RVR\Repository\UserRepository;
class UserController implements ISecured class UserController implements IAuthenticationRequired
{ {
private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository; private UserRepository $userRepository;
public function __construct(IRequest $request) public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
} }
public function authorize(): bool public function isAuthenticationRequired(): bool
{ {
return $this->request->user() !== null; return true;
} }
public function getAccount(): IContent public function getAccount(): IContent
@@ -40,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()
); );
@@ -75,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'])) {
@@ -94,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]);
} }
@@ -106,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,
@@ -119,18 +235,18 @@ 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]]);
} }
$newEmail = $this->request->post('email'); $newEmail = \Container::$request->post('email');
if ($newEmail !== $user->getEmail()) { if ($newEmail !== $user->getEmail()) {
if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) { if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) {
return new JsonContent(['error' => ['errorText' => 'Please provide a valid email address.']]); return new JsonContent(['error' => ['errorText' => 'Please provide a valid email address.']]);
@@ -143,7 +259,7 @@ class UserController implements ISecured
$user->setEmail($newEmail); $user->setEmail($newEmail);
} }
$newUsername = $this->request->post('username'); $newUsername = \Container::$request->post('username');
if ($newUsername !== $user->getUsername()) { if ($newUsername !== $user->getUsername()) {
if (strlen($newUsername) > 0) { if (strlen($newUsername) > 0) {
if (filter_var($newUsername, FILTER_VALIDATE_EMAIL)) { if (filter_var($newUsername, FILTER_VALIDATE_EMAIL)) {
@@ -160,7 +276,7 @@ class UserController implements ISecured
} }
} }
$newPassword = $this->request->post('password_new'); $newPassword = \Container::$request->post('password_new');
if (strlen($newPassword) > 0) { if (strlen($newPassword) > 0) {
if (strlen($newPassword) < 6) { if (strlen($newPassword) < 6) {
return new JsonContent([ return new JsonContent([
@@ -170,7 +286,7 @@ class UserController implements ISecured
]); ]);
} }
if ($newPassword !== $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.'
@@ -181,13 +297,13 @@ class UserController implements ISecured
$user->setPlainPassword($newPassword); $user->setPlainPassword($newPassword);
} }
$user->setNickname($this->request->post('nickname')); $user->setNickname(\Container::$request->post('nickname'));
$user->setPhone($this->request->post('phone')); $user->setPhone(\Container::$request->post('phone'));
$user->setIdNumber($this->request->post('id_number')); $user->setIdNumber(\Container::$request->post('id_number'));
$this->pdm->saveToDb($user); \Container::$persistentDataManager->saveToDb($user);
$this->request->session()->delete('authenticated_with_google_until'); \Container::$request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
+38
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
]);
}
}
+18
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();
}
}
+136
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
];
}
}
+52
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;
}
}
+85
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');
}
}
@@ -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;
}
}
+60
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;
}
}
@@ -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');
}
}
+107
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;
}
}
+1 -13
View File
@@ -7,7 +7,7 @@ class OAuthToken extends Model
{ {
protected static string $table = 'oauth_tokens'; protected static string $table = 'oauth_tokens';
protected static array $fields = ['scope', 'nonce', 'user_id', 'code', 'access_token', 'audience', 'created', 'expires']; protected static array $fields = ['scope', 'nonce', 'user_id', 'code', 'access_token', 'created', 'expires'];
protected static array $relations = ['user' => User::class]; protected static array $relations = ['user' => User::class];
@@ -25,8 +25,6 @@ class OAuthToken extends Model
private string $accessToken = ''; private string $accessToken = '';
private string $audience = '';
private DateTime $created; private DateTime $created;
private DateTime $expires; private DateTime $expires;
@@ -66,11 +64,6 @@ class OAuthToken extends Model
$this->accessToken = $accessToken; $this->accessToken = $accessToken;
} }
public function setAudience(string $audience): void
{
$this->audience = $audience;
}
public function setCreatedDate(DateTime $created): void public function setCreatedDate(DateTime $created): void
{ {
$this->created = $created; $this->created = $created;
@@ -126,11 +119,6 @@ class OAuthToken extends Model
return $this->accessToken; return $this->accessToken;
} }
public function getAudience(): string
{
return $this->audience;
}
public function getCreatedDate(): DateTime public function getCreatedDate(): DateTime
{ {
return $this->created; return $this->created;
+201
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');
}
}
@@ -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;
}
}
+5
View File
@@ -171,6 +171,11 @@ class User extends Model implements IUser
return $this->nickname ?: $this->fullName; 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
{ {
if ($this->password === null) { if ($this->password === null) {
@@ -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;
}
}
+16
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);
}
}
@@ -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);
}
}
+32
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);
}
}
+112
View File
@@ -0,0 +1,112 @@
<?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 = $this->selectAllByCommunity($community);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function countAllByCommunity(Community $community): int
{
return $this->selectAllByCommunity($community)->count();
}
public function getUpcomingAndRecentByCommunity(Community $community, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllByCommunity($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 = $this->selectAllByUser($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 = $this->selectAllByUser($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 = $this->selectAllByCommunity($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 = $this->selectAllByCommunity($community);
$select->where('title', 'LIKE', '%' . $title . '%');
$select->orderBy('start', 'DESC');
$select->limit(10);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class);
}
private function selectAllByCommunity(Community $community)
{
$select = new Select(Container::$dbConnection, Event::getTable());
$select->where('community_id', '=', $community->getId());
return $select;
}
private function selectAllByUser(User $user)
{
$select = new Select(Container::$dbConnection, Event::getTable());
$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());
return $select;
}
private function selectUpcomingAndRecent(Select $select, DateTime $from, int $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'));
});
}
}
+2 -10
View File
@@ -2,20 +2,12 @@
use SokoWeb\Database\Query\Select; use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\OAuthClient; use RVR\PersistentData\Model\OAuthClient;
use SokoWeb\PersistentData\PersistentDataManager;
class OAuthClientRepository class OAuthClientRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?OAuthClient public function getById(int $id): ?OAuthClient
{ {
return $this->pdm->selectFromDbById($id, OAuthClient::class); return \Container::$persistentDataManager->selectFromDbById($id, OAuthClient::class);
} }
public function getByClientId(string $clientId): ?OAuthClient public function getByClientId(string $clientId): ?OAuthClient
@@ -23,6 +15,6 @@ class OAuthClientRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('client_id', '=', $clientId); $select->where('client_id', '=', $clientId);
return $this->pdm->selectFromDb($select, OAuthClient::class); return \Container::$persistentDataManager->selectFromDb($select, OAuthClient::class);
} }
} }
+4 -12
View File
@@ -4,20 +4,12 @@ use DateTime;
use Generator; use Generator;
use SokoWeb\Database\Query\Select; use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\OAuthToken; use RVR\PersistentData\Model\OAuthToken;
use SokoWeb\PersistentData\PersistentDataManager;
class OAuthTokenRepository class OAuthTokenRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?OAuthToken public function getById(int $id): ?OAuthToken
{ {
return $this->pdm->selectFromDbById($id, OAuthToken::class); return \Container::$persistentDataManager->selectFromDbById($id, OAuthToken::class);
} }
public function getByCode(string $code): ?OAuthToken public function getByCode(string $code): ?OAuthToken
@@ -25,7 +17,7 @@ class OAuthTokenRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('code', '=', $code); $select->where('code', '=', $code);
return $this->pdm->selectFromDb($select, OAuthToken::class); return \Container::$persistentDataManager->selectFromDb($select, OAuthToken::class);
} }
public function getByAccessToken(string $accessToken): ?OAuthToken public function getByAccessToken(string $accessToken): ?OAuthToken
@@ -33,7 +25,7 @@ class OAuthTokenRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('access_token', '=', $accessToken); $select->where('access_token', '=', $accessToken);
return $this->pdm->selectFromDb($select, OAuthToken::class); return \Container::$persistentDataManager->selectFromDb($select, OAuthToken::class);
} }
public function getAllExpired(): Generator public function getAllExpired(): Generator
@@ -41,6 +33,6 @@ class OAuthTokenRepository
$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, OAuthToken::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class);
} }
} }
@@ -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);
}
}
+100
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;
}
}
@@ -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);
} }
} }
+15 -12
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,7 @@ 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 public function getByUsername(string $username): ?User
@@ -32,7 +25,7 @@ class UserRepository implements IUserRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('username', '=', $username); $select->where('username', '=', $username);
return $this->pdm->selectFromDb($select, User::class); return \Container::$persistentDataManager->selectFromDb($select, User::class);
} }
public function getByEmailOrUsername(string $emailOrUsername): ?User public function getByEmailOrUsername(string $emailOrUsername): ?User
@@ -49,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);
} }
} }
+41 -15
View File
@@ -4,38 +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-reload-on-success="true" data-observe-inputs="email,username,password_new,password_new_confirm,nickname,phone,id_number"> <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>
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" value="<?= $user['email'] ?>"> <p class="formLabel">Email address</p>
<input type="text" class="text big fullWidth marginTop" name="username" placeholder="Username" value="<?= $user['username'] ?>"> <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> <hr>
<input type="text" class="text big fullWidth marginTop" name="full_name" placeholder="Full name" value="<?= $user['full_name'] ?>" disabled> <p class="formLabel marginTop">Full name</p>
<input type="text" class="text big fullWidth marginTop" name="nickname" placeholder="Nickname" value="<?= $user['nickname'] ?>"> <input type="text" class="text big fullWidth" name="full_name" value="<?= $user['full_name'] ?>" disabled>
<input type="text" class="text big fullWidth marginTop" name="phone" placeholder="Phone" value="<?= $user['phone'] ?>"> <p class="formLabel marginTop marginTop">Nickname</p>
<input type="text" class="text big fullWidth marginTop" name="id_number" placeholder="ID number" value="<?= $user['id_number'] ?>"> <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
+1 -1
View File
@@ -5,7 +5,7 @@
@section(main) @section(main)
<h2>Authenticate with Google</h2> <h2>Authenticate with Google</h2>
<?php if (!$success): ?> <?php if (!$success): ?>
<div class="box"> <div class="box compactBox">
<p class="error justify"> <p class="error justify">
<?php if (isset($errorText)): ?> <?php if (isset($errorText)): ?>
<?= $errorText ?> <?= $errorText ?>
+22
View File
@@ -0,0 +1,22 @@
@extends(templates/layout_normal)
@section(main)
<h2>Connect with Google</h2>
<div class="box compactBox">
<?php if (!$success): ?>
<p class="error justify"><?= $error ?></p>
<?php else: ?>
<form id="connectGoogleForm" action="<?= Container::$routeCollection->getRoute('account.googleConnect-action')->generateLink() ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>">
<p class="justify marginBottom">Your account will be connected with the following Google account: <b><?= $googleAccount ?></b></p>
<input type="email" style="display: none;" name="email" autocomplete="username" value="<?= $userEmail ?>">
<p class="formLabel marginTop">Password</p>
<input type="password" class="text big fullWidth" name="password" autocomplete="current-password" required minlength="6" autofocus>
<p class="formError justify marginTop"></p>
<div class="right marginTop">
<button class="marginRight" type="submit" name="submit"><i class="fa-solid fa-link"></i> Connect</button><!--
--><a class="button gray" href="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>" title="Back to account">Cancel</a>
</div>
</form>
<?php endif; ?>
</div>
@endsection
+18
View File
@@ -0,0 +1,18 @@
@extends(templates/layout_normal)
@section(main)
<h2>Disconnect from Google</h2>
<div class="box compactBox">
<form id="connectGoogleForm" action="<?= Container::$routeCollection->getRoute('account.googleDisconnect-action')->generateLink() ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>">
<p class="justify marginBottom">Your account will be disconnected from the currently set Google account.</p>
<input type="email" style="display: none;" name="email" autocomplete="username" value="<?= $userEmail ?>">
<p class="formLabel marginTop">Password</p>
<input type="password" class="text big fullWidth" name="password" autocomplete="current-password" required minlength="6" autofocus>
<p class="formError justify marginTop"></p>
<div class="right marginTop">
<button class="red marginRight" type="submit" name="submit"><i class="fa-solid fa-link-slash"></i> Disconnect</button><!--
--><a class="button gray" href="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>" title="Back to account">Cancel</a>
</div>
</form>
</div>
@endsection
+63
View File
@@ -0,0 +1,63 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<?= $community->getName() ?>
<?php if ($editPermission): ?>
<span class="small">[<a href="<?= Container::$routeCollection->getRoute('community.edit')->generateLink(['communitySlug' => $community->getSlug()]) ?>">edit</a>]</span>
<?php endif; ?>
</h2>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.settings')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Settings</a></p>
<div class="gridContainer marginTop">
<div>
<h3 class="marginBottom">Upcoming and recent events</h3>
<?php if (count($upcomingAndRecentEvents) > 0): ?>
<?php foreach ($upcomingAndRecentEvents as $event): ?>
<p>
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a>
<span class="small"><?= $event->getStartDate()->format('Y-m-d') ?> – <?= $event->getEndDate()->format('Y-m-d') ?></span>
</p>
<?php endforeach; ?>
<?php else: ?>
<p>There is no event to show.</p>
<?php endif; ?>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">All events</a> | <a href="<?= Container::$routeCollection->getRoute('community.events.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New event</a></p>
</div>
<div>
<?php
$mainCurrencyCode = $community->getMainCurrency()->getCode();
$mainCurrencyRoundDigits = $community->getMainCurrency()->getRoundDigits();
?>
<h3 class="marginBottom">Finances</h3>
<p><a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Transactions</a> | <a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New transaction</a></p>
<table class="fullWidth marginTop">
<tr>
<td class="bold">You owe</td>
<td class="mono red" style="text-align: right;"><?= number_format($debtBalance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php foreach ($debtItems as $item): ?>
<tr>
<td class="small"><?= $item['payee']->getUser()->getDisplayName() ?></td>
<td class="small mono red" style="text-align: right;"><?= number_format($item['amount'], $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td class="bold">You're owed</td>
<td class="mono green" style="text-align: right;"><?= number_format($outstandingBalance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php foreach ($outstandingItems as $item): ?>
<tr>
<td class="small"><?= $item['payer']->getUser()->getDisplayName() ?></td>
<td class="small mono green" style="text-align: right;"><?= number_format($item['amount'], $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td class="bold">Your balance</td>
<td class="mono <?= $balance < 0 ? 'red' : ($balance > 0 ? 'green' : '') ?>" style="text-align: right;;"><?= number_format($balance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
</table>
</div>
</div>
@endsection
@@ -0,0 +1,51 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<a href="<?= Container::$routeCollection->getRoute('community.settings')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Settings</a> »
Edit currencies
</h2>
<div class="box compactBox">
<table class="fullWidth">
<thead>
<tr>
<th style="width: calc(100% / 3); text-align: left;">Code</th>
<th style="width: calc(100% / 3); text-align: left;">Round digits</th>
<th style="width: calc(100% / 3);"></th>
</tr>
</thead>
<?php foreach ($currencies as $currency): ?>
<tr>
<td>
<form id="editCurrency_<?= $currency->getId() ?>" action="<?= Container::$routeCollection->getRoute('community.currencies.edit-action')->generateLink(['communitySlug' => $community->getSlug(), 'currency_id' => $currency->getId()]) ?>" method="post" data-reload-on-success="true" data-observe-inputs="code,round_digits"></form>
<form id="deleteCurrency_<?= $currency->getId() ?>" action="<?= Container::$routeCollection->getRoute('community.currencies.delete-action')->generateLink(['communitySlug' => $community->getSlug(), 'currency_id' => $currency->getId()]) ?>" method="post" data-reload-on-success="true"></form>
<input type="text" form="editCurrency_<?= $currency->getId() ?>" class="text fullWidth" name="code" value="<?= $currency->getCode() ?>" maxlength="3" required>
</td>
<td>
<input type="number" form="editCurrency_<?= $currency->getId() ?>" class="text fullWidth" name="round_digits" value="<?= $currency->getRoundDigits() ?>" min="0" max="9" required>
</td>
<td style="text-align: right; font-size: 0;">
<button type="submit" form="editCurrency_<?= $currency->getId() ?>" name="submit_button" class="small" disabled><i class="fa-regular fa-floppy-disk"></i></button>
<?php if ($currency->getId() !== $community->getMainCurrencyId()): ?>
<button type="submit" form="deleteCurrency_<?= $currency->getId() ?>" name="submit_button" data-confirmation="Are you sure you want to delete this currency?" data-confirmation-button='<i class="fa-regular fa-trash-can"></i> Delete' class="small red marginLeft"><i class="fa-regular fa-trash-can"></i></button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<tr>
<td>
<form id="newCurrency" action="<?= Container::$routeCollection->getRoute('community.currencies.new-action')->generateLink(['communitySlug' => $community->getSlug()]) ?>" method="post" data-reload-on-success="true" data-observe-inputs="code,round_digits"></form>
<input type="text" form="newCurrency" class="text fullWidth" name="code" maxlength="3" required>
</td>
<td>
<input type="number" form="newCurrency" class="text fullWidth" name="round_digits" min="0" max="9" required>
</td>
<td style="text-align: right;">
<button type="submit" form="newCurrency" name="submit_button" class="small" disabled><i class="fa-regular fa-plus"></i></button>
</td>
</tr>
</table>
<p class="formError justify marginTop"></p>
</div>
@endsection
+39
View File
@@ -0,0 +1,39 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<?php if (isset($community)): ?>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
Edit
<?php else: ?>
New community
<?php endif; ?>
</h2>
<div class="box compactBox">
<?php
$formAction = isset($community) ?
Container::$routeCollection->getRoute('community.edit-action')->generateLink(['communitySlug' => $community->getSlug()]) :
Container::$routeCollection->getRoute('community.new-action')->generateLink();
?>
<form id="communityForm" action="<?= $formAction ?>" method="post" data-redirect-on-success="true">
<p class="formLabel">Name</p>
<input type="text" class="text big fullWidth" name="name" value="<?= isset($community) ? $community->getName() : '' ?>" required>
<?php if (!isset($community)): ?>
<p class="formLabel marginTop">Main currency code</p>
<input type="text" class="text big fullWidth" name="main_currency_code" maxlength="3" required>
<p class="formLabel marginTop">Main currency round digits</p>
<input type="number" class="text big fullWidth" name="main_currency_round_digits" min="0" max="9" required>
<?php endif; ?>
<p id="communityFormError" class="formError justify marginTop"></p>
<div class="right marginTop" style="font-size: 0;">
<button type="submit" name="submit_button"><?= isset($community) ? '<i class="fa-regular fa-floppy-disk"></i> Save' : '<i class="fa-regular fa-plus"></i> Create' ?></button>
<?php if (isset($community)): ?>
<button type="submit" form="deleteCommunity" name="submit_button" data-confirmation="Are you sure you want to delete this community?" class="red marginLeft"><i class="fa-regular fa-trash-can"></i> Delete</button>
<?php endif; ?>
</div>
</form>
<?php if (isset($community)): ?>
<form id="deleteCommunity" action="<?= Container::$routeCollection->getRoute('community.delete-action')->generateLink(['communitySlug' => $community->getSlug()]) ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('home')->generateLink() ?>"></form>
<?php endif; ?>
</div>
@endsection
+62
View File
@@ -0,0 +1,62 @@
@css(node_modules/tom-select/dist/css/tom-select.min.css)
@js(node_modules/tom-select/dist/js/tom-select.base.min.js)
@js(js/communities/community_members.js)
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<a href="<?= Container::$routeCollection->getRoute('community.settings')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Settings</a> »
Edit members
</h2>
<div class="box compactBox">
<table class="fullWidth">
<thead>
<tr>
<th style="width: 50%;">User</th>
<th style="width: 25%; text-align: center;">Owner</th>
<th style="width: 25%;"></th>
</tr>
</thead>
<?php foreach ($members as $member): ?>
<?php $editable = $member->getUserId() !== Container::$request->user()->getUniqueId(); ?>
<tr>
<td>
<form id="editMember_<?= $member->getId() ?>" action="<?= Container::$routeCollection->getRoute('community.members.edit-action')->generateLink(['communitySlug' => $community->getSlug(), 'community_member_id' => $member->getId()]) ?>" method="post" data-reload-on-success="true" data-observe-inputs="owner"></form>
<form id="deleteMember_<?= $member->getId() ?>" action="<?= Container::$routeCollection->getRoute('community.members.delete-action')->generateLink(['communitySlug' => $community->getSlug(), 'community_member_id' => $member->getId()]) ?>" method="post" data-reload-on-success="true"></form>
<?= $member->getUser()->getDisplayName() ?>
</td>
<td style="text-align: center;">
<input type="checkbox" form="editMember_<?= $member->getId() ?>" name="owner" <?= $member->getOwner() ? 'checked' : '' ?> <?= !$editable ? 'disabled' : '' ?> />
</td>
<td style="text-align: right;">
<?php if ($editable): ?>
<button type="submit" form="editMember_<?= $member->getId() ?>" name="submit_button" class="small marginRight" disabled><i class="fa-regular fa-floppy-disk"></i></button><!--
--><button type="submit" form="deleteMember_<?= $member->getId() ?>" name="submit_button" data-confirmation="Are you sure you want to delete this member?" data-confirmation-button='<i class="fa-regular fa-trash-can"></i> Delete' class="small red"><i class="fa-regular fa-trash-can"></i></button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<tr>
<td>
<form id="newMember" action="<?= Container::$routeCollection->getRoute('community.members.new-action')->generateLink(['communitySlug' => $community->getSlug()]) ?>" method="post" data-reload-on-success="true" data-observe-inputs="user_id"></form>
<select form="newMember" name="user_id" required></select>
</td>
<td style="text-align: center;">
<input type="checkbox" form="newMember" name="owner" />
</td>
<td style="text-align: right;">
<button type="submit" form="newMember" name="submit_button" class="small" disabled><i class="fa-regular fa-plus"></i></button>
</td>
</tr>
</table>
<p class="formError justify marginTop"></p>
</div>
@endsection
@section(pageScript)
<script>
var searchUserUrl = '<?= Container::$routeCollection->getRoute('searchUser')->generateLink(['q' => 'QUERY']) ?>';
</script>
@endsection
+39
View File
@@ -0,0 +1,39 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
Settings
</h2>
<div class="gridContainer marginTop">
<div>
<h3 class="marginBottom">
Members
<?php if ($editPermission): ?>
<span class="small">[<a href="<?= Container::$routeCollection->getRoute('community.members')->generateLink(['communitySlug' => $community->getSlug()]) ?>">edit</a>]</span>
<?php endif; ?>
</h3>
<?php foreach ($members as $member): ?>
<p><?= $member->getUser()->getDisplayName() ?></p>
<?php endforeach; ?>
</div>
<div>
<h3 class="marginBottom">
Currencies
<?php if ($editPermission): ?>
<span class="small">[<a href="<?= Container::$routeCollection->getRoute('community.currencies')->generateLink(['communitySlug' => $community->getSlug()]) ?>">edit</a>]</span>
<?php endif; ?>
</h3>
<?php foreach ($currencies as $currency): ?>
<p>
<?php if ($currency->getId() === $community->getMainCurrencyId()): ?>
<b><?= $currency->getCode() ?></b>
<?php else: ?>
<a href="<?= Container::$routeCollection->getRoute('community.currencyExchangeRates')->generateLink(['communitySlug' => $community->getSlug(), 'code' => $currency->getCode()]) ?>"><?= $currency->getCode() ?></a>
<?php endif; ?>
</p>
<?php endforeach; ?>
</div>
</div>
@endsection
@@ -0,0 +1,61 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<a href="<?= Container::$routeCollection->getRoute('community.settings')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Settings</a> »
Exchange rates for <?= $currency->getCode() ?>
</h2>
<div class="box compactBox">
<table class="fullWidth">
<thead>
<tr>
<?php if ($editPermission): ?>
<th style="width: 25%; text-align: left;">Exchange rate</th>
<th style="width: 50%; text-align: left;">Valid from</th>
<th style="width: 25%;"></th>
<?php else: ?>
<th style="width: 35%; text-align: left;">Exchange rate</th>
<th style="width: 65%; text-align: left;">Valid from</th>
<?php endif; ?>
</tr>
</thead>
<?php foreach ($currencyExchangeRates as $currencyExchangeRate): ?>
<tr>
<?php if ($editPermission): ?>
<td>
<form id="editExchangeRate_<?= $currencyExchangeRate->getId() ?>" action="<?= Container::$routeCollection->getRoute('community.currencyExchangeRates.edit-action')->generateLink(['communitySlug' => $community->getSlug(), 'code' => $currency->getCode(), 'currency_exchange_rate_id' => $currencyExchangeRate->getId()]) ?>" method="post" data-reload-on-success="true" data-observe-inputs="exchange_rate,valid_from"></form>
<form id="deleteCurrency_<?= $currencyExchangeRate->getId() ?>" action="<?= Container::$routeCollection->getRoute('community.currencyExchangeRates.delete-action')->generateLink(['communitySlug' => $community->getSlug(), 'code' => $currency->getCode(), 'currency_exchange_rate_id' => $currencyExchangeRate->getId()]) ?>" method="post" data-reload-on-success="true"></form>
<input type="number" form="editExchangeRate_<?= $currencyExchangeRate->getId() ?>" class="text fullWidth" name="exchange_rate" value="<?= $currencyExchangeRate->getExchangeRate() ?>" min="0" step="0.000000001" required>
</td>
<td>
<input type="datetime-local" form="editExchangeRate_<?= $currencyExchangeRate->getId() ?>" class="text fullWidth" name="valid_from" value="<?= $currencyExchangeRate->getValidFromDate()->format('Y-m-d\TH:i') ?>" required>
</td>
<td style="text-align: right;">
<button type="submit" form="editExchangeRate_<?= $currencyExchangeRate->getId() ?>" name="submit_button" class="small marginRight" disabled><i class="fa-regular fa-floppy-disk"></i></button><!--
--><button type="submit" form="deleteCurrency_<?= $currencyExchangeRate->getId() ?>" name="submit_button" data-confirmation="Are you sure you want to delete this exchange rate?" data-confirmation-button='<i class="fa-regular fa-trash-can"></i> Delete' class="small red"><i class="fa-regular fa-trash-can"></i></button>
</td>
<?php else: ?>
<td><?= $currencyExchangeRate->getExchangeRate() ?></td>
<td><?= $currencyExchangeRate->getValidFromDate()->format('Y-m-d H:i') ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php if ($editPermission): ?>
<tr>
<td>
<form id="newExchangeRate" action="<?= Container::$routeCollection->getRoute('community.currencyExchangeRates.new-action')->generateLink(['communitySlug' => $community->getSlug(), 'code' => $currency->getCode()]) ?>" method="post" data-reload-on-success="true" data-observe-inputs="exchange_rate,valid_from"></form>
<input type="number" form="newExchangeRate" class="text fullWidth" name="exchange_rate" min="0" step="0.000000001" required>
</td>
<td>
<input type="datetime-local" form="newExchangeRate" class="text fullWidth" name="valid_from" required>
</td>
<td style="text-align: right;">
<button type="submit" form="newExchangeRate" name="submit_button" class="small" disabled><i class="fa-regular fa-plus"></i></button>
</td>
</tr>
<?php endif; ?>
</table>
<p class="formError justify marginTop"></p>
</div>
@endsection
+87
View File
@@ -0,0 +1,87 @@
@css(node_modules/tom-select/dist/css/tom-select.min.css)
@js(node_modules/tom-select/dist/js/tom-select.base.min.js)
@js(js/communities/transaction.js)
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<?php if (isset($event)): ?>
<a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Events</a> »
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a> »
<?php endif; ?>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'event' => isset($event) ? $event->getSlug() : null]) ?>">Transactions</a> »
<?php if (isset($transaction)): ?>
Edit transaction
<?php else: ?>
New transaction
<?php endif; ?>
</h2>
<div class="box compactBox">
<?php
$formAction = isset($transaction) ?
Container::$routeCollection->getRoute('community.transactions.edit-action')->generateLink(['communitySlug' => $community->getSlug(), 'transactionId' => $transaction->getId()]) :
Container::$routeCollection->getRoute('community.transactions.new-action')->generateLink(['communitySlug' => $community->getSlug()]);
?>
<form id="transactionForm" action="<?= $formAction ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'event' => isset($event) ? $event->getSlug() : null]) ?>">
<p class="formLabel">Event</p>
<select name="event_id">
<option value="">[none]</option>
<?php if (isset($event)): ?>
<option value="<?= $event->getId() ?>" selected><?= $event->getTitle() ?></option>
<?php endif; ?>
</select>
<p class="formLabel marginTop">Payer</p>
<select class="big fullWidth" name="payer_user_id" required>
<option value="" hidden></option>
<?php foreach ($members as $member): ?>
<option value="<?= $member->getUser()->getId() ?>"
<?= isset($transaction) ?
($transaction->getPayerUserId() === $member->getUser()->getId() ? 'selected' : '') :
(Container::$request->user()->getUniqueId() == $member->getUser()->getId() ? 'selected' : '') ?>>
<?= $member->getUser()->getDisplayName() ?>
</option>
<?php endforeach; ?>
</select>
<p class="formLabel marginTop">Payee(s)</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-gap: 10px;">
<?php foreach ($members as $member): ?>
<div style="text-align: center;">
<input id="payee_<?= $member->getUserId() ?>" type="checkbox" name="payee_user_ids[]" value="<?= $member->getUserId() ?>" <?= !isset($transaction) || count($payeeUserIds) === 0 || in_array($member->getUserId(), $payeeUserIds) ? 'checked' : '' ?>><!--
--><label for="payee_<?= $member->getUserId() ?>"><?= $member->getUser()->getDisplayName() ?></label>
</div>
<?php endforeach; ?>
</div>
<p class="formLabel marginTop">Description</p>
<input type="text" class="text big fullWidth" name="description" value="<?= isset($transaction) ? $transaction->getDescription() : '' ?>" required>
<p class="formLabel marginTop">Sum</p>
<input type="number" class="text big fullWidth" name="sum" value="<?= isset($transaction) ? $transaction->getSum() : '' ?>" min="0" step="0.000000001" required>
<p class="formLabel marginTop">Currency</p>
<select class="big fullWidth" name="currency_id" required>
<option value="" hidden></option>
<?php foreach ($currencies as $currency): ?>
<option value="<?= $currency->getId() ?>" <?= isset($transaction) && $transaction->getCurrencyId() === $currency->getId() ? 'selected' : '' ?>><?= $currency->getCode() ?></option>
<?php endforeach; ?>
</select>
<p class="formLabel marginTop">Time</p>
<input type="datetime-local" class="text big fullWidth" name="time" value="<?= isset($transaction) ? $transaction->getTimeDate()->format('Y-m-d\TH:i') : (new DateTime())->format('Y-m-d\TH:i') ?>" required>
<p class="formError justify marginTop"></p>
<div class="right marginTop" style="font-size: 0;">
<button type="submit" name="submit_button"><?= isset($transaction) ? '<i class="fa-regular fa-floppy-disk"></i> Save' : '<i class="fa-regular fa-plus"></i> Create' ?></button>
<?php if (isset($transaction)): ?>
<button type="submit" form="deleteTransaction" name="submit_button" data-confirmation="Are you sure you want to delete this transaction?" class="red marginLeft"><i class="fa-regular fa-trash-can"></i> Delete</button>
<?php endif; ?>
</div>
</form>
<?php if (isset($transaction)): ?>
<form id="deleteTransaction" action="<?= Container::$routeCollection->getRoute('community.transactions.delete-action')->generateLink(['communitySlug' => $community->getSlug(), 'transactionId' => $transaction->getId()]) ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug()]) ?>"></form>
<?php endif; ?>
</div>
@endsection
@section(pageScript)
<script>
var searchEventUrl = '<?= Container::$routeCollection->getRoute('community.events.search')->generateLink(['communitySlug' => $community->getSlug(), 'q' => 'QUERY']) ?>';
</script>
@endsection
+78
View File
@@ -0,0 +1,78 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<?php if (isset($event)): ?>
<a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Events</a> »
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a> »
<?php endif; ?>
Transactions
</h2>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug(), 'event' => isset($event) ? $event->getSlug() : null]) ?>">New transaction</a></p>
<?php if ($numberOfTransactions > 0): ?>
<?php
$paginationRouteId = 'community.transactions';
$paginationRouteParams = ['communitySlug' => $community->getSlug()];
?>
<?php if ($pages > 1): ?>
@include(templates/pagination)
<?php endif; ?>
<?php foreach ($transactions as $transaction): ?>
<a class="block" href="<?= Container::$routeCollection->getRoute('community.transactions.edit')->generateLink(['communitySlug' => $community->getSlug(), 'transactionId' => $transaction->getId()]) ?>">
<div class="box transaction">
<div>
<?php if ($transaction->getEvent()): ?>
<p><span class="label"><?= $transaction->getEvent()->getTitle() ?></span></p>
<?php endif; ?>
<p style="font-weight: bold;"><?= $transaction->getDescription() ?></p>
<p class="small">
<?= $transaction->getPayerUser()->getDisplayName() ?>
<i class="fa-solid fa-caret-right"></i>
<?php foreach ($members as $member): ?>
<?php
if (count($transaction->getPayees()) > 0) {
$found = false;
foreach ($transaction->getPayees() as $payee) {
if ($member->getUserId() === $payee->getUserId()) {
$found = true;
}
}
} else {
$found = true;
}
?>
<?php if ($found): ?>
<?= $member->getUser()->getDisplayName() ?>
<?php else: ?>
<span class="gray" style="text-decoration: line-through;"><?= $member->getUser()->getDisplayName() ?></span>
<?php endif; ?>
<?php endforeach; ?>
</p>
<p class="small"><?= $transaction->getTimeDate()->format('Y-m-d H:i') ?></p>
</div>
<div style="text-align: right;">
<h3><?= number_format($exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate()), $community->getMainCurrency()->getRoundDigits()) ?> <?= $community->getMainCurrency()->getCode() ?></h3>
<?php if ($community->getMainCurrencyId() !== $transaction->getCurrencyId()): ?>
<p style="color: #8e8e8e; font-weight: bold;"><?= number_format($transaction->getSum(), $transaction->getCurrency()->getRoundDigits()) ?> <?= $transaction->getCurrency()->getCode() ?></p>
<?php endif; ?>
</div>
</div>
</a>
<?php endforeach; ?>
<?php if ($pages > 1): ?>
@include(templates/pagination)
<?php endif; ?>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug(), 'event' => isset($event) ? $event->getSlug() : null]) ?>">New transaction</a></p>
<?php else: ?>
<div class="box">
<p>There are no transactions yet.</p>
</div>
<?php endif; ?>
@endsection
+1 -1
View File
@@ -2,5 +2,5 @@
@section(main) @section(main)
<h2>404 | Page not found</h2> <h2>404 | Page not found</h2>
<p>The requested URL was not found on this server. <a href="/" title="<?= $_ENV['APP_NAME'] ?>">Back to start.</a></p> <p>The requested URL was not found on this server. <a href="<?= Container::$routeCollection->getRoute('home')->generateLink() ?>" title="<?= $_ENV['APP_NAME'] ?>">Back to start.</a></p>
@endsection @endsection
+11
View File
@@ -0,0 +1,11 @@
@extends(templates/layout_normal)
@section(main)
<h2>500 | Internal server error</h2>
<p>An error occured during processing your request. <a href="<?= Container::$routeCollection->getRoute('home')->generateLink() ?>" title="<?= $_ENV['APP_NAME'] ?>">Back to start.</a></p>
<?php if (isset($exceptionToPrint)): ?>
<pre class="marginTop">
<?= $exceptionToPrint ?>
</pre>
<?php endif; ?>
@endsection
+8
View File
@@ -0,0 +1,8 @@
@extends(templates/layout_normal)
@section(main)
<h2>No event found</h2>
<div class="box compactBox">
<p class="error justify">No upcoming or recent event was found. <a href="<?= Container::$routeCollection->getRoute('home')->generateLink() ?>" title="<?= $_ENV['APP_NAME'] ?>">Back to start.</a></p>
</div>
@endsection
+54
View File
@@ -0,0 +1,54 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Events</a> »
<?= $event->getTitle() ?>
<span class="small">[<a href="<?= Container::$routeCollection->getRoute('community.event.edit')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>">edit</a>]</span>
</h2>
<div class="gridContainer marginTop">
<div>
<?php
$mainCurrencyCode = $community->getMainCurrency()->getCode();
$mainCurrencyRoundDigits = $community->getMainCurrency()->getRoundDigits();
?>
<h3 class="marginBottom">Finances</h3>
<p><a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'event' => $event->getSlug()]) ?>">Transactions</a> | <a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug(), 'event' => $event->getSlug()]) ?>">New transaction</a></p>
<table class="fullWidth marginTop">
<tr>
<td class="bold">Total cost</td>
<td class="mono" style="text-align: right;"><?= number_format($totalCost, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
</table>
<table class="fullWidth marginTop">
<tr>
<td class="bold">You owe*</td>
<td class="mono red" style="text-align: right;"><?= number_format($debtBalance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php foreach ($debtItems as $item): ?>
<tr>
<td class="small"><?= $item['payee']->getUser()->getDisplayName() ?></td>
<td class="small mono red" style="text-align: right;"><?= number_format($item['amount'], $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td class="bold">You're owed*</td>
<td class="mono green" style="text-align: right;"><?= number_format($outstandingBalance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php foreach ($outstandingItems as $item): ?>
<tr>
<td class="small"><?= $item['payer']->getUser()->getDisplayName() ?></td>
<td class="small mono green" style="text-align: right;"><?= number_format($item['amount'], $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td class="bold">Your balance*</td>
<td class="mono <?= $balance < 0 ? 'red' : ($balance > 0 ? 'green' : '') ?>" style="text-align: right;;"><?= number_format($balance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
</table>
<p class="small right">* Virtual balance only for this event. Check <a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>">community</a> finances for your real balance.</p>
</div>
</div>
@endsection
+40
View File
@@ -0,0 +1,40 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Events</a> »
<?php if (isset($event)): ?>
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a> » Edit event
<?php else: ?>
New event
<?php endif; ?>
</h2>
<div class="box compactBox">
<?php
$formAction = isset($event) ?
Container::$routeCollection->getRoute('community.event.edit-action')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) :
Container::$routeCollection->getRoute('community.events.new-action')->generateLink(['communitySlug' => $community->getSlug()]);
?>
<form id="eventForm" action="<?= $formAction ?>" method="post" data-redirect-on-success="true">
<p class="formLabel">Title</p>
<input type="text" class="text big fullWidth" name="title" value="<?= isset($event) ? $event->getTitle() : '' ?>" required>
<p class="formLabel marginTop">Description</p>
<textarea class="text big fullWidth" name="description" rows="3"><?= isset($event) ? $event->getDescription() : '' ?></textarea>
<p class="formLabel marginTop">Start</p>
<input type="datetime-local" class="text big fullWidth" name="start" value="<?= isset($event) ? $event->getStartDate()->format('Y-m-d\TH:i') : '' ?>" required>
<p class="formLabel marginTop">End</p>
<input type="datetime-local" class="text big fullWidth" name="end" value="<?= isset($event) ? $event->getEndDate()->format('Y-m-d\TH:i') : '' ?>" required>
<p class="formError justify marginTop"></p>
<div class="right marginTop" style="font-size: 0;">
<button type="submit" name="submit_button"><?= isset($event) ? '<i class="fa-regular fa-floppy-disk"></i> Save' : '<i class="fa-regular fa-plus"></i> Create' ?></button>
<?php if (isset($event)): ?>
<button type="submit" form="deleteEvent" name="submit_button" data-confirmation="Are you sure you want to delete this event? All linked transactions will be unlinked!" class="red marginLeft"><i class="fa-regular fa-trash-can"></i> Delete</button>
<?php endif; ?>
</div>
</form>
<?php if (isset($event)): ?>
<form id="deleteEvent" action="<?= Container::$routeCollection->getRoute('community.event.delete-action')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>"></form>
<?php endif; ?>
</div>
@endsection
+41
View File
@@ -0,0 +1,41 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
Events
</h2>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.events.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New event</a></p>
<?php if ($numberOfEvents > 0): ?>
<?php
$paginationRouteId = 'community.events';
$paginationRouteParams = ['communitySlug' => $community->getSlug()];
?>
<?php if ($pages > 1): ?>
@include(templates/pagination)
<?php endif; ?>
<?php foreach ($events as $event): ?>
<a class="block" href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>">
<div class="box">
<p style="font-weight: bold;"><?= $event->getTitle() ?></p>
<p class="small"><?= $event->getStartDate()->format('Y-m-d') ?> – <?= $event->getEndDate()->format('Y-m-d') ?></p>
<p class="small"><?= $event->getDescription() ?></p>
</div>
</a>
<?php endforeach; ?>
<?php if ($pages > 1): ?>
@include(templates/pagination)
<?php endif; ?>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.events.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New event</a></p>
<?php else: ?>
<div class="box">
<p>There are no events yet.</p>
</div>
<?php endif; ?>
@endsection
+35
View File
@@ -0,0 +1,35 @@
@extends(templates/layout_normal)
@section(main)
<div class="gridContainer">
<div>
<h3 class="marginBottom">
Communities
<span class="small">[<a href="<?= Container::$routeCollection->getRoute('community.new')->generateLink() ?>">new</a>]</span>
</h3>
<?php if (count($communities) > 0): ?>
<?php foreach ($communities as $community): ?>
<p><a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a></p>
<?php endforeach; ?>
<?php else: ?>
<p>You have no community.</p>
<?php endif; ?>
</div>
<div>
<h3 class="marginBottom">Upcoming and recent events</h3>
<?php if (count($upcomingAndRecentEvents) > 0): ?>
<?php foreach ($upcomingAndRecentEvents as $event): ?>
<p>
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $event->getCommunity()->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a>
<span class="small">
(<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $event->getCommunity()->getSlug()]) ?>"><?= $event->getCommunity()->getName() ?></a>)
<?= $event->getStartDate()->format('Y-m-d') ?> – <?= $event->getEndDate()->format('Y-m-d') ?>
</span>
</p>
<?php endforeach; ?>
<?php else: ?>
<p>There is no event to show.</p>
<?php endif; ?>
</div>
</div>
@endsection
-8
View File
@@ -1,8 +0,0 @@
@extends(templates/layout_normal)
@section(main)
<h2><?= $_ENV['APP_NAME'] ?></h2>
<div class="box">
<p>This is the new <?= $_ENV['APP_NAME'] ?>.</p>
</div>
@endsection
+2 -2
View File
@@ -1,8 +1,8 @@
@extends(templates/layout_normal) @extends(templates/layout_normal)
@section(main) @section(main)
<h2>Login up with Google</h2> <h2>Login with Google</h2>
<div class="box"> <div class="box compactBox">
<p class="error justify"><?= $error ?></p> <p class="error justify"><?= $error ?></p>
</div> </div>
@endsection @endsection
+9 -7
View File
@@ -2,18 +2,20 @@
@section(main) @section(main)
<h2>Login</h2> <h2>Login</h2>
<div class="box"> <div class="box compactBox">
<form id="loginForm" action="/login" method="post" data-redirect-on-success="<?= $redirectUrl ?>"> <form id="loginForm" action="<?= Container::$routeCollection->getRoute('login-action')->generateLink() ?>" method="post" data-redirect-on-success="<?= $redirectUrl ?>">
<input type="text" class="text big fullWidth" name="email" placeholder="Email address / Username" required autofocus> <p class="formLabel">Email address / Username</p>
<input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password" required minlength="6"> <input type="text" class="text big fullWidth" name="email" autocomplete="username" required autofocus>
<p class="formLabel marginTop">Password</p>
<input type="password" class="text big fullWidth" name="password" autocomplete="current-password" required minlength="6">
<p id="loginFormError" class="formError justify marginTop"></p> <p id="loginFormError" class="formError justify marginTop"></p>
<div class="right marginTop"> <div class="right marginTop">
<button type="submit">Login</button> <button type="submit"><i class="fa-solid fa-arrow-right-to-bracket"></i> Login</button>
</div> </div>
<p class="center marginTop"><a href="/password/requestReset" title="Request password reset">Forgot your password?</a></p> <p class="center marginTop"><a href="<?= Container::$routeCollection->getRoute('password.requestReset')->generateLink() ?>" title="Request password reset">Forgot your password?</a></p>
<hr> <hr>
<div class="center"> <div class="center">
<a class="button yellow" href="/login/google" title="Login with Google">Login with Google</a> <a class="button yellow" href="<?= Container::$routeCollection->getRoute('login.google')->generateLink() ?>" title="Login with Google"><i class="fa-brands fa-google"></i> Login with Google</a>
</div> </div>
</form> </form>
</div> </div>
+5 -4
View File
@@ -4,9 +4,10 @@
@section(main) @section(main)
<h2>Request password reset</h2> <h2>Request password reset</h2>
<div class="box"> <div class="box compactBox">
<form id="passwordResetForm" action="/password/requestReset" method="post" data-redirect-on-success="/password/requestReset/success"> <form id="passwordResetForm" action="<?= Container::$routeCollection->getRoute('password.requestReset-action')->generateLink() ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('password.requestReset.success')->generateLink() ?>">
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" value="<?= isset($email) ? $email : '' ?>" required autofocus> <p class="formLabel">Email address</p>
<input type="email" class="text big fullWidth" name="email" value="<?= isset($email) ? $email : '' ?>" required autofocus>
<?php if (!empty($_ENV['RECAPTCHA_SITEKEY'])): ?> <?php if (!empty($_ENV['RECAPTCHA_SITEKEY'])): ?>
<div class="marginTop"> <div class="marginTop">
<div class="g-recaptcha" data-sitekey="<?= $_ENV['RECAPTCHA_SITEKEY'] ?>"></div> <div class="g-recaptcha" data-sitekey="<?= $_ENV['RECAPTCHA_SITEKEY'] ?>"></div>
@@ -14,7 +15,7 @@
<?php endif; ?> <?php endif; ?>
<p id="passwordResetFormError" class="formError justify marginTop"></p> <p id="passwordResetFormError" class="formError justify marginTop"></p>
<div class="right marginTop"> <div class="right marginTop">
<button type="submit">Continue</button> <button type="submit"><i class="fa-solid fa-key"></i> Reset password</button>
</div> </div>
</form> </form>
</div> </div>
@@ -2,7 +2,7 @@
@section(main) @section(main)
<h2>Request password reset</h2> <h2>Request password reset</h2>
<div class="box"> <div class="box compactBox">
<p class="justify">Password reset was successfully requested. Please check your email and click on the link to reset your password!</p> <p class="justify">Password reset was successfully requested. Please check your email and click on the link to reset your password!</p>
</div> </div>
@endsection @endsection
+10 -7
View File
@@ -2,19 +2,22 @@
@section(main) @section(main)
<h2>Reset password</h2> <h2>Reset password</h2>
<div class="box"> <div class="box compactBox">
<?php if ($success) : ?> <?php if ($success) : ?>
<form id="resetPasswordForm" action="/password/reset/<?= $token ?>" method="post" data-redirect-on-success="<?= $redirectUrl ?>"> <form id="resetPasswordForm" action="<?= Container::$routeCollection->getRoute('password.reset-action')->generateLink(['token' => $token]) ?>"" method="post" data-redirect-on-success="<?= $redirectUrl ?>">
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" value="<?= $email ?>" disabled> <p class="formLabel">Email address</p>
<input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password" required minlength="6" autofocus> <input type="text" class="text big fullWidth" name="email" autocomplete="username" value="<?= $email ?>" disabled>
<input type="password" class="text big fullWidth marginTop" name="password_confirm" placeholder="Password confirmation" required minlength="6"> <p class="formLabel marginTop">Password</p>
<input type="password" class="text big fullWidth" name="password" autocomplete="new-password" required minlength="6" autofocus>
<p class="formLabel marginTop">Password confirmation</p>
<input type="password" class="text big fullWidth" name="password_confirm" autocomplete="new-password" required minlength="6">
<p id="resetPasswordFormError" class="formError justify marginTop"></p> <p id="resetPasswordFormError" class="formError justify marginTop"></p>
<div class="right"> <div class="right">
<button class="marginTop" type="submit">Reset password</button> <button class="marginTop" type="submit"><i class="fa-solid fa-key"></i> Reset password</button>
</div> </div>
</form> </form>
<?php else: ?> <?php else: ?>
<p class="error justify">Confirming your identity failed. Please check the link you entered, or retry <a href="/password/requestReset" title="Request password reset">requesting password reset</a>!</p> <p class="error justify">Confirming your identity failed. Please check the link you entered, or retry <a href="<?= Container::$routeCollection->getRoute('password.requestReset')->generateLink() ?>" title="Request password reset">requesting password reset</a>!</p>
<?php endif; ?> <?php endif; ?>
</div> </div>
@endsection @endsection
+1 -1
View File
@@ -2,7 +2,7 @@
@section(main) @section(main)
<h2>OAuth error</h2> <h2>OAuth error</h2>
<div class="box"> <div class="box compactBox">
<p class="error justify"><?= $error ?></p> <p class="error justify"><?= $error ?></p>
</div> </div>
@endsection @endsection
+6 -8
View File
@@ -3,21 +3,19 @@
@section(content) @section(content)
<header> <header>
<h1> <h1>
<a href="/" title="<?= $_ENV['APP_NAME'] ?>"> <a href="<?= Container::$routeCollection->getRoute('home')->generateLink() ?>" title="<?= $_ENV['APP_NAME'] ?>">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>" alt="<?= $_ENV['APP_NAME'] ?>"><!-- <img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>" alt="<?= $_ENV['APP_NAME'] ?>"><!--
--><span><?= $_ENV['APP_NAME'] ?></span> --><span><?= $_ENV['APP_NAME'] ?></span>
</a> </a>
</h1> </h1>
<p> <p>
<?php if (Container::$request->user()) : ?> <?php if (Container::$request->user()) : ?>
<span><a href="/account" title="Account"> <span><a href="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>" title="Account"><!--
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?> --><i class="fa-regular fa-user"></i> <?= Container::$request->user()->getDisplayName() ?><!--
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg><!--
--><?= Container::$request->user()->getDisplayName() ?><!--
--></a></span><!-- --></a></span><!--
--><span><a href="/logout" title="Logout">Logout</a></span> --><span><a href="<?= Container::$routeCollection->getRoute('logout')->generateLink() ?>" title="Logout"><!--
--><i class="fa-solid fa-arrow-right-from-bracket"></i> Logout</a><!--
--></span>
<?php endif; ?> <?php endif; ?>
</p> </p>
</header> </header>
+31
View File
@@ -0,0 +1,31 @@
<p class="paginateContainer marginTop">
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => 1])) ?>">«</a>
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => max(1, $currentPage - 1)])) ?>">‹</a>
<?php
$maxPages = 7;
if ($pages <= $maxPages) {
$start = 1;
$end = $pages;
} else {
$maxAdditionalPages = $maxPages - 1;
$maxAdditionalPagesHalf = ceil($maxAdditionalPages / 2);
$start = $currentPage - $maxAdditionalPagesHalf > 1 ?
($currentPage - $maxAdditionalPagesHalf < $pages - $maxAdditionalPages ?
$currentPage - $maxAdditionalPagesHalf :
$pages - $maxAdditionalPages) :
1;
$end = $start + $maxAdditionalPages < $pages ?
$start + $maxAdditionalPages :
$pages;
}
?>
<?php for ($i = $start; $i <= $end; $i++): ?>
<?php if ($currentPage == $i): ?>
<span class="selected"><?= $i ?></span>
<?php else: ?>
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => $i])) ?>"><?= $i ?></a>
<?php endif; ?>
<?php endfor; ?>
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => min($pages, $currentPage + 1)])) ?>">›</a>
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => $pages])) ?>">»</a>
</p>
+9 -4
View File
@@ -5,18 +5,23 @@
<meta name="description" content="<?= $_ENV['APP_NAME'] ?>"> <meta name="description" content="<?= $_ENV['APP_NAME'] ?>">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#212f4d"> <meta name="theme-color" content="#212f4d">
<title><?= $_ENV['APP_NAME'] ?></title> <title><?= isset($title) ? $title . ' – ' : '' ?><?= $_ENV['APP_NAME'] ?></title>
<?php if (preg_match('/^(http(s)?:)?\/\//', $_ENV['STATIC_ROOT']) === 1): ?> <?php if (preg_match('/^(http(s)?:)?\/\//', $_ENV['STATIC_ROOT']) === 1): ?>
<link href="<?= $_ENV['STATIC_ROOT'] ?>" rel="preconnect"> <link href="<?= $_ENV['STATIC_ROOT'] ?>" rel="preconnect">
<?php endif; ?> <?php endif; ?>
<link href="https://fonts.gstatic.com" rel="preconnect"> <link href="https://fonts.googleapis.com" rel="preconnect">
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin>
<?php if (!empty($_ENV['GOOGLE_ANALITICS_ID'])): ?> <?php if (!empty($_ENV['GOOGLE_ANALITICS_ID'])): ?>
<link href="https://www.googletagmanager.com" rel="preconnect"> <link href="https://www.googletagmanager.com" rel="preconnect">
<link href="https://www.google-analytics.com" rel="preconnect"> <link href="https://www.google-analytics.com" rel="preconnect">
<?php endif; ?> <?php endif; ?>
<link href="https://fonts.googleapis.com/css2?family=Oxygen:wght@400;700&amp;display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Oxygen:wght@400;700&amp;display=block" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Oxygen+Mono:wght@400&amp;display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Oxygen+Mono:wght@400&amp;display=block" rel="stylesheet">
<link href="<?= $_ENV['STATIC_ROOT'] ?>/css/rvr.css?rev=<?= REVISION ?>" rel="stylesheet"> <link href="<?= $_ENV['STATIC_ROOT'] ?>/css/rvr.css?rev=<?= REVISION ?>" rel="stylesheet">
<link href="<?= $_ENV['STATIC_ROOT'] ?>/node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css?rev=<?= REVISION ?>" rel="stylesheet">
<link href="<?= $_ENV['STATIC_ROOT'] ?>/node_modules/@fortawesome/fontawesome-free/css/solid.min.css?rev=<?= REVISION ?>" rel="stylesheet">
<link href="<?= $_ENV['STATIC_ROOT'] ?>/node_modules/@fortawesome/fontawesome-free/css/regular.min.css?rev=<?= REVISION ?>" rel="stylesheet">
<link href="<?= $_ENV['STATIC_ROOT'] ?>/node_modules/@fortawesome/fontawesome-free/css/brands.min.css?rev=<?= REVISION ?>" rel="stylesheet">
@yields('externalCss') @yields('externalCss')
@yields('inlineCss') @yields('inlineCss')
<link rel="icon" type="image/png" sizes="192x192" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/192x192.png?rev=<?= REVISION ?>"> <link rel="icon" type="image/png" sizes="192x192" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/192x192.png?rev=<?= REVISION ?>">
+125 -48
View File
@@ -1,5 +1,22 @@
<?php <?php
use SokoWeb\Response\HttpResponse;
use SokoWeb\Routing\RouteCollection;
use SokoWeb\Session\DatabaseSessionHandler;
use SokoWeb\Request\Request;
use SokoWeb\Request\Session;
use RVR\Controller\HomeController;
use RVR\Controller\LoginController;
use RVR\Controller\OAuthAuthController;
use RVR\Controller\OAuthController;
use RVR\Controller\UserController;
use RVR\Controller\UserSearchController;
use RVR\Controller\CommunityController;
use RVR\Controller\TransactionController;
use RVR\Controller\EventRedirectController;
use RVR\Controller\EventController;
use RVR\Repository\UserRepository;
require 'app.php'; require 'app.php';
if (!empty($_ENV['DEV'])) { if (!empty($_ENV['DEV'])) {
@@ -10,75 +27,135 @@ if (!empty($_ENV['DEV'])) {
ini_set('display_errors', '0'); ini_set('display_errors', '0');
} }
Container::$routeCollection = new SokoWeb\Routing\RouteCollection(); Container::$routeCollection = new RouteCollection();
Container::$routeCollection->get('index', '', [RVR\Controller\HomeController::class, 'getIndex']); Container::$routeCollection->get('home', '', [HomeController::class, 'getHome']);
Container::$routeCollection->get('startSession', 'startSession.json', [RVR\Controller\HomeController::class, 'startSession']); Container::$routeCollection->group('login', function (RouteCollection $routeCollection) {
Container::$routeCollection->group('login', function (SokoWeb\Routing\RouteCollection $routeCollection) { $routeCollection->get('login', '', [LoginController::class, 'getLoginForm']);
$routeCollection->get('login', '', [RVR\Controller\LoginController::class, 'getLoginForm']); $routeCollection->post('login-action', '', [LoginController::class, 'login']);
$routeCollection->post('login-action', '', [RVR\Controller\LoginController::class, 'login']); $routeCollection->get('login.google', 'google', [LoginController::class, 'getGoogleLoginRedirect']);
$routeCollection->get('login-google', 'google', [RVR\Controller\LoginController::class, 'getGoogleLoginRedirect']); $routeCollection->get('login.google-action', 'google/code', [LoginController::class, 'loginWithGoogle']);
$routeCollection->get('login-google-action', 'google/code', [RVR\Controller\LoginController::class, 'loginWithGoogle']);
}); });
Container::$routeCollection->group('oauth', function (SokoWeb\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('oauth', function (RouteCollection $routeCollection) {
$routeCollection->get('oauth-auth', 'auth', [RVR\Controller\OAuthAuthController::class, 'auth']); $routeCollection->get('oauth.auth', 'auth', [OAuthAuthController::class, 'auth']);
$routeCollection->post('oauth-token', 'token', [RVR\Controller\OAuthController::class, 'getToken']); $routeCollection->post('oauth.token', 'token', [OAuthController::class, 'getToken']);
$routeCollection->get('oauth-userinfo', 'userinfo', [RVR\Controller\OAuthController::class, 'getUserInfo']); $routeCollection->get('oauth.userinfo', 'userinfo', [OAuthController::class, 'getUserInfo']);
$routeCollection->get('oauth-config', '.well-known/openid-configuration', [RVR\Controller\OAuthController::class, 'getConfig']); $routeCollection->get('oauth.config', '.well-known/openid-configuration', [OAuthController::class, 'getConfig']);
$routeCollection->get('oauth-certs', 'certs', [RVR\Controller\OAuthController::class, 'getCerts']); $routeCollection->get('oauth.certs', 'certs', [OAuthController::class, 'getCerts']);
}); });
Container::$routeCollection->group('password', function (SokoWeb\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('password', function (RouteCollection $routeCollection) {
$routeCollection->get('password-requestReset', 'requestReset', [RVR\Controller\LoginController::class, 'getRequestPasswordResetForm']); $routeCollection->get('password.requestReset', 'requestReset', [LoginController::class, 'getRequestPasswordResetForm']);
$routeCollection->post('password-requestReset-action', 'requestReset', [RVR\Controller\LoginController::class, 'requestPasswordReset']); $routeCollection->post('password.requestReset-action', 'requestReset', [LoginController::class, 'requestPasswordReset']);
$routeCollection->get('password-requestReset.success', 'requestReset/success', [RVR\Controller\LoginController::class, 'getRequestPasswordResetSuccess']); $routeCollection->get('password.requestReset.success', 'requestReset/success', [LoginController::class, 'getRequestPasswordResetSuccess']);
$routeCollection->get('password-reset', 'reset/{token}', [RVR\Controller\LoginController::class, 'getResetPasswordForm']); $routeCollection->get('password.reset', 'reset/{token}', [LoginController::class, 'getResetPasswordForm']);
$routeCollection->post('password-reset.action', 'reset/{token}', [RVR\Controller\LoginController::class, 'resetPassword']); $routeCollection->post('password.reset-action', 'reset/{token}', [LoginController::class, 'resetPassword']);
}); });
Container::$routeCollection->get('logout', 'logout', [RVR\Controller\LoginController::class, 'logout']); Container::$routeCollection->get('logout', 'logout', [LoginController::class, 'logout']);
Container::$routeCollection->group('account', function (SokoWeb\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('account', function (RouteCollection $routeCollection) {
$routeCollection->get('account', '', [RVR\Controller\UserController::class, 'getAccount']); $routeCollection->get('account', '', [UserController::class, 'getAccount']);
$routeCollection->post('account-action', '', [RVR\Controller\UserController::class, 'saveAccount']); $routeCollection->post('account-action', '', [UserController::class, 'saveAccount']);
$routeCollection->get('account.googleAuthenticate', 'googleAuthenticate', [RVR\Controller\UserController::class, 'getGoogleAuthenticateRedirect']); $routeCollection->get('account.googleConnect', 'googleConnect', [UserController::class, 'getGoogleConnectRedirect']);
$routeCollection->get('account.googleAuthenticate-action', 'googleAuthenticate/code', [RVR\Controller\UserController::class, 'authenticateWithGoogle']); $routeCollection->get('account.googleConnect-confirm', 'googleConnect/code', [UserController::class, 'getGoogleConnectConfirm']);
$routeCollection->post('account.googleConnect-action', 'googleConnect', [UserController::class, 'connectGoogle']);
$routeCollection->get('account.googleDisconnect', 'googleDisconnect', [UserController::class, 'getGoogleDisconnectConfirm']);
$routeCollection->post('account.googleDisconnect-action', 'googleDisconnect', [UserController::class, 'disconnectGoogle']);
$routeCollection->get('account.googleAuthenticate', 'googleAuthenticate', [UserController::class, 'getGoogleAuthenticateRedirect']);
$routeCollection->get('account.googleAuthenticate-action', 'googleAuthenticate/code', [UserController::class, 'authenticateWithGoogle']);
});
Container::$routeCollection->get('searchUser', 'searchUser', [UserSearchController::class, 'searchUser']);
Container::$routeCollection->group('now', function (RouteCollection $routeCollection) {
$routeCollection->get('now.event', '', [EventRedirectController::class, 'getEvent']);
$routeCollection->get('now.transactions.new', 'transactions/new', [EventRedirectController::class, 'getEventNewTransaction']);
});
Container::$routeCollection->group('communities', function (RouteCollection $routeCollection) {
$routeCollection->get('community.new', 'new', [CommunityController::class, 'getCommunityNew']);
$routeCollection->post('community.new-action', 'new', [CommunityController::class, 'saveCommunity']);
$routeCollection->group('{communitySlug}', function (RouteCollection $routeCollection) {
$routeCollection->get('community', '', [CommunityController::class, 'getCommunityHome']);
$routeCollection->get('community.settings', 'settings', [CommunityController::class, 'getCommunitySettings']);
$routeCollection->get('community.edit', 'edit', [CommunityController::class, 'getCommunityEdit']);
$routeCollection->post('community.edit-action', 'edit', [CommunityController::class, 'saveCommunity']);
$routeCollection->post('community.delete-action', 'delete', [CommunityController::class, 'deleteCommunity']);
$routeCollection->get('community.members', 'members', [CommunityController::class, 'getMembersEdit']);
$routeCollection->post('community.members.new-action', 'members/new', [CommunityController::class, 'saveMember']);
$routeCollection->post('community.members.edit-action', 'members/edit', [CommunityController::class, 'saveMember']);
$routeCollection->post('community.members.delete-action', 'members/delete', [CommunityController::class, 'deleteMember']);
$routeCollection->get('community.currencies', 'currencies', [CommunityController::class, 'getCurrenciesEdit']);
$routeCollection->post('community.currencies.new-action', 'currencies/new', [CommunityController::class, 'saveCurrency']);
$routeCollection->post('community.currencies.edit-action', 'currencies/edit', [CommunityController::class, 'saveCurrency']);
$routeCollection->post('community.currencies.delete-action', 'currencies/delete', [CommunityController::class, 'deleteCurrency']);
$routeCollection->group('currencyExchangeRates', function (RouteCollection $routeCollection) {
$routeCollection->get('community.currencyExchangeRates', '{code}', [CommunityController::class, 'getCurrencyExchangeRates']);
$routeCollection->post('community.currencyExchangeRates.new-action', '{code}/new', [CommunityController::class, 'saveCurrencyExchangeRate']);
$routeCollection->post('community.currencyExchangeRates.edit-action', '{code}/edit', [CommunityController::class, 'saveCurrencyExchangeRate']);
$routeCollection->post('community.currencyExchangeRates.delete-action', '{code}/delete', [CommunityController::class, 'deleteCurrencyExchangeRate']);
});
$routeCollection->group('transactions', function (RouteCollection $routeCollection) {
$routeCollection->get('community.transactions', '', [TransactionController::class, 'getTransactions']);
$routeCollection->get('community.transactions.new', 'new', [TransactionController::class, 'getTransactionEdit']);
$routeCollection->post('community.transactions.new-action', 'new', [TransactionController::class, 'saveTransaction']);
$routeCollection->get('community.transactions.edit', '{transactionId}', [TransactionController::class, 'getTransactionEdit']);
$routeCollection->post('community.transactions.edit-action', '{transactionId}', [TransactionController::class, 'saveTransaction']);
$routeCollection->post('community.transactions.delete-action', '{transactionId}/delete', [TransactionController::class, 'deleteTransaction']);
});
$routeCollection->group('events', function (RouteCollection $routeCollection) {
$routeCollection->get('community.events', '', [EventController::class, 'getEvents']);
$routeCollection->get('community.events.new', 'new', [EventController::class, 'getEventEdit']);
$routeCollection->post('community.events.new-action', 'new', [EventController::class, 'saveEvent']);
$routeCollection->get('community.events.search', 'search', [EventController::class, 'searchEvent']);
$routeCollection->group('{eventSlug}', function (RouteCollection $routeCollection) {
$routeCollection->get('community.event', '', [EventController::class, 'getEvent']);
$routeCollection->get('community.event.edit', 'edit', [EventController::class, 'getEventEdit']);
$routeCollection->post('community.event.edit-action', 'edit', [EventController::class, 'saveEvent']);
$routeCollection->post('community.event.delete-action', 'delete', [EventController::class, 'deleteEvent']);
});
});
});
}); });
Container::$sessionHandler = new SokoWeb\Session\DatabaseSessionHandler(); Container::$sessionHandler = new DatabaseSessionHandler(
Container::$dbConnection,
'sessions',
new DateTime('-7 days')
);
session_set_save_handler(Container::$sessionHandler, true); session_set_save_handler(Container::$sessionHandler, true);
session_start([ session_start([
'gc_probability' => 0, // old sessions are deleted by MaintainDatabaseCommand 'gc_probability' => 0, // old sessions are deleted by MaintainDatabaseCommand
'cookie_lifetime' => 604800, 'cookie_lifetime' => 0,
'cookie_path' => '/', 'cookie_path' => '/',
'cookie_httponly' => true, 'cookie_httponly' => true,
'cookie_samesite' => 'Lax' 'cookie_samesite' => 'Lax'
]); ]);
if (isset($_COOKIE[session_name()])) { Container::$request = new Request(
// extend session cookie lifetime is cookie already exists
setcookie(session_name(), session_id(), [
'expires' => time() + 604800,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax'
]);
}
// this is needed to handle old type of session IDs
if (!Container::$sessionHandler->validateId(session_id())) {
session_regenerate_id(true);
}
Container::$request = new SokoWeb\Request\Request(
$_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'],
$_GET, $_GET,
$_POST, $_POST,
$_SESSION, getallheaders(),
new RVR\Repository\UserRepository() new Session($_SESSION),
new UserRepository()
); );
if (!Container::$request->session()->has('anti_csrf_token')) { if (!Container::$request->session()->has('anti_csrf_token')) {
Container::$request->session()->set('anti_csrf_token', bin2hex(random_bytes(16))); Container::$request->session()->set('anti_csrf_token', bin2hex(random_bytes(16)));
} }
//TODO: make a nicer logic $appConfig = [
$antiCsrfTokenExceptions = ['oauth/token']; 'antiCsrfTokenName' => 'anti_csrf_token',
'antiCsrfTokenErrorResponse' => ['error' => 'no_valid_anti_csrf_token'],
'antiCsrfTokenExceptions' => ['/oauth/token'],
'loginRouteId' => 'login',
'error404View' => 'error/404',
'error500View' => 'error/500'
];
$httpReponse = new HttpResponse(
Container::$request,
Container::$dbConnection,
Container::$routeCollection,
$appConfig,
$_SERVER['REQUEST_METHOD'],
$_SERVER['REQUEST_URI']
);
$httpReponse->render();