Compare commits

...

579 Commits

Author SHA1 Message Date
MaysWind 55d06640b5 auto hide forget password sheet after email has been resent 2023-09-17 19:37:26 +08:00
MaysWind 3874a8da21 clear email when open forget password sheet 2023-09-17 19:36:36 +08:00
MaysWind 6a3ecd5b09 add cancel button to forget password sheet 2023-09-17 19:34:20 +08:00
MaysWind afcd4f7262 support resending verify mail on mobile device 2023-09-17 19:32:12 +08:00
MaysWind b0ae731fa6 update golang to 1.20.8 2023-09-17 18:12:55 +08:00
MaysWind 0b678fe69a code refactor 2023-09-17 18:07:09 +08:00
MaysWind 4cecc78a74 don't show verify email has sent to when there are no valid verify tokens 2023-09-17 18:01:10 +08:00
MaysWind 92273d2fc6 add skip_tls_verify option for exchange rates 2023-09-17 17:24:41 +08:00
MaysWind 04ec749c3c fix the bug that old uuid may be generated sometimes 2023-09-16 23:53:45 +08:00
MaysWind 165377816c return error when uuid is not enough 2023-09-16 22:45:00 +08:00
MaysWind 729904e1c3 remove unused code 2023-09-15 21:54:25 +08:00
MaysWind 6bc4fa0a82 update vuetify to 3.3.16 2023-09-15 21:41:28 +08:00
MaysWind f12403672b update README 2023-09-10 23:30:44 +08:00
MaysWind 97daff8d4f update mobile screenshot img url 2023-09-10 22:12:36 +08:00
MaysWind a32451fd7f hide resend verify email button when server disables verify email 2023-09-10 17:29:10 +08:00
MaysWind ca14770971 return error entity when verify email is not enabled 2023-09-10 17:25:23 +08:00
MaysWind 35ac0695c7 don't show send mail tips when force verify email is enabled but verify email is not enabled 2023-09-10 17:15:56 +08:00
MaysWind 589b614a53 remove old email verify token before send new verify email when email changed 2023-09-10 17:00:19 +08:00
MaysWind 64ea3e05d8 aysnc send email 2023-09-10 16:49:24 +08:00
MaysWind 5d0e115438 auto send verify email when user email has been changed 2023-09-10 16:40:48 +08:00
MaysWind 9f35c1eded improve user registration page 2023-09-10 16:21:29 +08:00
MaysWind ff07346fe9 modify style 2023-09-10 00:25:52 +08:00
MaysWind 22fffc2f8c profile page supports resending verify email 2023-09-10 00:25:42 +08:00
MaysWind 205363dd42 change link text when email is verified 2023-09-09 21:33:17 +08:00
MaysWind d2297b882f send verify email after account has been registered 2023-09-09 21:28:17 +08:00
MaysWind 48bf8dbc5b don't create temporary token when smtp is not enabled 2023-09-09 21:25:30 +08:00
MaysWind 9585c760d5 modify style 2023-09-09 16:39:28 +08:00
MaysWind 8c2cd0aa4d modify style 2023-09-09 15:56:21 +08:00
MaysWind 2e680b04c9 supports building for different platforms for gitea actions 2023-09-04 23:33:52 +08:00
MaysWind e2b81f7b57 add email verification 2023-09-04 23:30:33 +08:00
MaysWind c38b277887 disabled user cannot use forget password 2023-09-03 23:35:01 +08:00
MaysWind a1f6304b22 fix log content is wrong when content has % symbol 2023-09-03 23:15:40 +08:00
MaysWind 09d7f56efc modify log formatter 2023-09-03 13:56:19 +08:00
MaysWind 9221f3fc96 add request id to sql query log 2023-09-03 13:54:07 +08:00
MaysWind 6b30a0aebc supports building multiple image by gitea actions 2023-09-02 19:33:48 +08:00
MaysWind 158563f387 code refactor 2023-09-02 00:05:31 +08:00
MaysWind b0fc5752e2 upgrade vuetify to 3.3.15 2023-09-01 23:28:18 +08:00
MaysWind 28903615ed fix the problem that unit test would not run sometimes 2023-08-28 23:06:03 +08:00
MaysWind caa83c0432 modify text 2023-08-28 00:16:16 +08:00
MaysWind 045f4a42db replace the app name in the email with the one configured in the config file 2023-08-28 00:11:42 +08:00
MaysWind 3275bc9cae redirect page to login page after reset password successfully 2023-08-27 23:58:00 +08:00
MaysWind 6d14eaefe1 code refactor 2023-08-27 23:51:26 +08:00
MaysWind 03274725be modify text and field name 2023-08-27 23:28:38 +08:00
MaysWind 0951006063 add updating user email verified state utility 2023-08-27 22:54:34 +08:00
MaysWind 616bfc6a2a not allow send password reset mail when email address is not verified 2023-08-27 22:35:16 +08:00
MaysWind c0bfe429ee the language of password reset email set to client language if user language is not set 2023-08-27 22:29:54 +08:00
MaysWind c1d90485a1 reset password don't set authorization header 2023-08-27 22:21:10 +08:00
MaysWind 6c30527684 fix typo 2023-08-27 21:37:22 +08:00
MaysWind 4ac751f492 add send user password reset email command line utility 2023-08-27 21:37:10 +08:00
MaysWind de7b137257 add send test email utility 2023-08-27 21:26:30 +08:00
MaysWind 0bf689fa8d code refactor 2023-08-27 21:23:03 +08:00
MaysWind f31ef1649f support reset password by email reset link 2023-08-27 21:22:52 +08:00
MaysWind c66bc62c41 code refactor 2023-08-26 23:35:53 +08:00
MaysWind 9ee59215c8 code refactor 2023-08-26 23:14:42 +08:00
MaysWind a991adecaf modify style 2023-08-26 19:36:39 +08:00
MaysWind 601b1d5c89 modify style 2023-08-26 19:10:06 +08:00
MaysWind 8b593883a7 modify style 2023-08-26 18:22:19 +08:00
MaysWind bb549c9a89 modify text 2023-08-26 17:57:42 +08:00
MaysWind 9f2005622a fix color is wrong in dark theme 2023-08-26 17:11:29 +08:00
MaysWind 4238bde13a modify style 2023-08-26 00:21:00 +08:00
MaysWind 2d4ce1aac0 modify style 2023-08-26 00:12:29 +08:00
MaysWind 37269abde2 update go.sum 2023-08-26 00:02:05 +08:00
MaysWind 54038eabd4 upgrade vuetify to 3.3.14 2023-08-26 00:01:12 +08:00
MaysWind 57922e3071 modify style 2023-08-25 23:59:05 +08:00
MaysWind ad36dfd448 auto choose the secondary category of selected primary category when create transaction in transaction list page 2023-08-22 00:57:02 +08:00
MaysWind 6e334d2efb hide the category dropdown menu when click the item 2023-08-22 00:05:36 +08:00
MaysWind d3dc1401fd auto hide the dropdown menu when click the menu item 2023-08-21 23:59:48 +08:00
MaysWind 0f59c9911d modify style 2023-08-21 23:54:36 +08:00
MaysWind 0ebfd2bc76 modify style 2023-08-21 23:51:24 +08:00
MaysWind 734625c1e3 modify style 2023-08-21 23:46:34 +08:00
MaysWind eaaea8a64f modify props and fields name 2023-08-21 23:42:07 +08:00
MaysWind c026ab1777 modify style 2023-08-21 23:41:10 +08:00
MaysWind 2c7193efea don't allow show the transaction detail when the transaction type is modify balance 2023-08-21 23:35:59 +08:00
MaysWind 21f5ef469b always display account name even if the account is hidden 2023-08-21 23:35:21 +08:00
MaysWind e7f9eb6e06 modify style 2023-08-21 23:25:15 +08:00
MaysWind 9db9c382ab modify text 2023-08-21 23:16:12 +08:00
MaysWind b2ba626cde hide the buttons which is not supported 2023-08-21 23:15:45 +08:00
MaysWind 85d93c4f4b auto hide the dropdown menu when click the secondary item 2023-08-21 23:02:46 +08:00
MaysWind 09f6dd8d82 add more log in unit test 2023-08-21 01:41:03 +08:00
MaysWind 2aa6df48c6 remove unused code 2023-08-21 01:16:43 +08:00
MaysWind 2a16260b05 transaction edit dialog supports duplicate transaction, edit transaction and save new transaction 2023-08-21 01:11:01 +08:00
MaysWind c28158b041 remove unused code 2023-08-21 00:00:25 +08:00
MaysWind 661199850c remove unused code 2023-08-20 23:53:41 +08:00
MaysWind 67b69a45cc modify style 2023-08-20 23:33:14 +08:00
MaysWind 579322578b modify style 2023-08-20 22:21:50 +08:00
MaysWind 0de65042b0 transaction edit dialog supports transaction category 2023-08-20 20:31:29 +08:00
MaysWind f9643f8651 modify method name 2023-08-20 19:43:22 +08:00
MaysWind fc9ac4c40e modify method name 2023-08-20 19:41:51 +08:00
MaysWind 84843066f2 fix style 2023-08-20 19:23:13 +08:00
MaysWind af3d5c4654 remove unused code 2023-08-20 17:35:11 +08:00
MaysWind 676f3daf50 modify style 2023-08-20 17:30:47 +08:00
MaysWind 54970015cb update third party dependencies 2023-08-20 17:17:41 +08:00
MaysWind 2b49430530 update golang to 1.20.7, update node.js to 18.17.1, update alpine base image to 3.17.5 2023-08-20 17:01:01 +08:00
MaysWind feea5f3518 modify style 2023-08-20 16:57:00 +08:00
MaysWind 04e580b40f modify style 2023-08-20 01:22:00 +08:00
MaysWind f07a3ba97d code refactor 2023-08-20 01:09:23 +08:00
MaysWind f44e3a81ab modify style 2023-08-20 01:08:26 +08:00
MaysWind 77d3bd019e fix the problem that the thousands separator is missing 2023-08-20 00:57:01 +08:00
MaysWind 72725d5ab4 hide set as base currency when the currency is already the base 2023-08-20 00:54:11 +08:00
MaysWind aa717ed1fe fix the problem that the time zone of the modify balance transaction generated by creating a new account was wrong 2023-08-20 00:48:42 +08:00
MaysWind 292b49ba79 modify style 2023-08-20 00:40:56 +08:00
MaysWind c600eb5d5a modify style 2023-08-20 00:33:57 +08:00
MaysWind 6d2f788fa2 code refactor 2023-08-20 00:30:55 +08:00
MaysWind b8acff3e7c code refactor 2023-08-20 00:30:30 +08:00
MaysWind b8bdb03fc0 transaction edit dialog supports transaction time 2023-08-19 22:41:13 +08:00
MaysWind 7257abefb4 fix AM/PM text in datetime picker is not translated 2023-08-19 22:37:26 +08:00
MaysWind 3095613a76 transaction edit dialog supports transaction tags 2023-08-18 00:44:05 +08:00
MaysWind db12b64b3a modify style 2023-08-18 00:03:49 +08:00
MaysWind a081edde25 modify style 2023-08-17 23:01:52 +08:00
MaysWind 5496c4a10a code refactor 2023-08-16 00:51:12 +08:00
MaysWind c968ded99a transaction edit dialog supports map 2023-08-16 00:51:03 +08:00
MaysWind 4224a833b4 code refactor 2023-08-16 00:04:50 +08:00
MaysWind df470f1a5e modify style 2023-08-15 22:53:09 +08:00
MaysWind ed0100a82c add more transaction edit dialog basis code 2023-08-15 01:05:59 +08:00
MaysWind 0ad72e8334 modify style 2023-08-14 23:13:03 +08:00
MaysWind 50ee5d1f49 hide control buttons when loading 2023-08-14 23:12:35 +08:00
MaysWind 94283a8da2 update unit test 2023-08-14 22:47:54 +08:00
MaysWind 86e9a3e838 add transaction edit dialog basis code 2023-08-14 01:03:43 +08:00
MaysWind d77b9ef7c9 code refactor 2023-08-14 00:55:41 +08:00
MaysWind e29afa3155 code refactor 2023-08-14 00:14:53 +08:00
MaysWind 7376fbe7a1 code refactor 2023-08-13 23:58:28 +08:00
MaysWind 04e98e1c39 code refactor 2023-08-13 23:40:29 +08:00
MaysWind ddca6e7ec9 code refactor 2023-08-13 23:33:07 +08:00
MaysWind 1ac12ac668 remove unused code 2023-08-13 23:03:30 +08:00
MaysWind 399936e3ab code refactor 2023-08-13 22:58:52 +08:00
MaysWind 3d086992dc modify style 2023-08-13 22:52:31 +08:00
MaysWind 62ded1290c code refactor 2023-08-13 22:19:59 +08:00
MaysWind 33cbdfbd13 code refactor 2023-08-13 20:48:32 +08:00
MaysWind f5ce6ed4bc code refactor 2023-08-13 20:32:51 +08:00
MaysWind eb9a2ac2e0 code refactor 2023-08-13 20:13:27 +08:00
MaysWind 8bed529d05 code refactor 2023-08-13 20:06:42 +08:00
MaysWind 41a8b8007a code refactor 2023-08-13 20:01:44 +08:00
MaysWind 141dc843f3 modify method name 2023-08-13 19:52:58 +08:00
MaysWind 902825404e support disable boot log 2023-08-13 18:28:08 +08:00
MaysWind 749eaaab30 add parsing request id command utility 2023-08-13 18:19:09 +08:00
MaysWind 8f5767b992 add unit test 2023-08-13 17:27:25 +08:00
MaysWind 715f0c5853 add unittest 2023-08-13 17:08:36 +08:00
MaysWind a960fd3d56 improve unit test 2023-08-13 16:52:20 +08:00
MaysWind 17b8ac6d0b fix gravatar url is invalid when email contains upper characters 2023-08-13 15:37:48 +08:00
MaysWind 4ac78fe4d1 modify method name 2023-08-13 15:30:20 +08:00
MaysWind 957bcf790f code refactor 2023-08-13 15:18:38 +08:00
MaysWind 66f0b38008 code refactor 2023-08-13 15:03:31 +08:00
MaysWind f6a2246aab code refactor 2023-08-13 14:58:11 +08:00
MaysWind fa3e941069 show login link in sign up page 2023-08-13 14:46:06 +08:00
MaysWind 9bb049f746 modify style 2023-08-13 14:21:53 +08:00
MaysWind c41f2a4d65 modify style 2023-08-13 14:20:40 +08:00
MaysWind 06ff9f2499 add add/edit account dialog 2023-08-13 02:00:14 +08:00
MaysWind f91f9fcc94 modify style 2023-08-13 01:35:11 +08:00
MaysWind 9a626b0d4f modify dialog style 2023-08-13 01:34:40 +08:00
MaysWind ac2adaf4ba modify style 2023-08-12 18:16:53 +08:00
MaysWind 3ab198615b code refactor 2023-08-12 13:05:03 +08:00
MaysWind 7c2831098c clear displayed transactions when changing filter 2023-08-12 00:28:32 +08:00
MaysWind 2454e22ea2 support setting items per page in transaction list page 2023-08-11 23:58:30 +08:00
MaysWind b1fb40ca61 modify style 2023-08-08 01:39:50 +08:00
MaysWind e403e938c3 show category comment in list 2023-08-08 01:17:59 +08:00
MaysWind 952ba1f1ea code refactor 2023-08-07 01:32:11 +08:00
MaysWind a25690c2d3 modify style 2023-08-07 01:31:34 +08:00
MaysWind 6b6b9c61d7 auto complete supports auto selected the first item by enter / tab key 2023-08-07 01:14:47 +08:00
MaysWind fcc5e522a7 modify style 2023-08-07 01:10:07 +08:00
MaysWind c33c0487cf add add/edit transaction category dialog 2023-08-07 01:00:02 +08:00
MaysWind 1753a6c247 manually set default icon color 2023-08-07 00:48:48 +08:00
MaysWind 195f513416 code refactor 2023-08-07 00:38:08 +08:00
MaysWind c6d38bb3d7 code refactor 2023-08-06 23:57:23 +08:00
MaysWind 9b2fba9600 fix the problem that not scroll to selected item in color selection sheet or icon icon selection sheet 2023-08-06 23:35:51 +08:00
MaysWind c88f6501fa don't auto hide sheet when select icon 2023-08-06 23:30:10 +08:00
MaysWind c511346160 add missing component 2023-08-06 20:42:36 +08:00
MaysWind 0b02daac1d modify file name 2023-08-06 18:58:05 +08:00
MaysWind 2390c085e4 modify style 2023-08-06 18:52:55 +08:00
MaysWind 698d94feed code refactor 2023-08-05 23:04:29 +08:00
MaysWind a9a6d39127 update vuetify to 3.3.11 2023-08-05 17:14:59 +08:00
MaysWind 395bd31898 move files 2023-08-05 16:51:34 +08:00
MaysWind 7e24492ce8 add category preset for desktop page 2023-08-04 01:00:19 +08:00
MaysWind 19d3d80013 add category preset for desktop page 2023-08-04 00:56:26 +08:00
MaysWind 8c7875d7ea modify text 2023-08-03 18:21:19 +08:00
MaysWind 110ce0d4c6 modify skeleton style 2023-08-02 00:53:12 +08:00
MaysWind ff8c57fdab add transaction category list page 2023-08-02 00:49:37 +08:00
MaysWind 54ccdc57bf modify field name 2023-08-02 00:24:59 +08:00
MaysWind 2463f06ba1 only show add default categories button when really have no category 2023-08-02 00:14:09 +08:00
MaysWind 88d5b1f98f set drag handle disabled when loading or updating 2023-08-02 00:01:12 +08:00
MaysWind 43522e9c81 add all date type range in transaction list page 2023-08-01 22:33:12 +08:00
MaysWind 6e798f739f support click in trend chart in overview page 2023-08-01 22:15:15 +08:00
MaysWind 5b5f1280af fix the problem that the transaction date not display sometimes in transaction list page 2023-08-01 21:47:26 +08:00
MaysWind 2188e8dd78 fix wrong timeout 2023-08-01 09:20:02 +08:00
MaysWind 8659e9ea37 add asset summary card in home page, add 6 more months in trend card 2023-08-01 09:19:33 +08:00
MaysWind 4cff481d61 fix the problem that the monthly total income/expense amount sometimes is wrong 2023-07-31 10:05:30 +08:00
MaysWind 6da910d8fb modify style 2023-07-31 01:22:35 +08:00
MaysWind cc08ad46e3 modify overview page loading style 2023-07-31 01:21:47 +08:00
MaysWind 5b52c1adbb show no data in trend card when there are really no data in recent 6 months 2023-07-30 23:57:47 +08:00
MaysWind a20958a89b adjust display order of expense and income 2023-07-30 23:30:05 +08:00
MaysWind dea36d4b80 add trend in income and expense card in overview page 2023-07-30 23:26:10 +08:00
MaysWind 6cb7e4caf7 modify style 2023-07-30 22:59:03 +08:00
MaysWind 6e41668b25 add more runtime caching pattern 2023-07-30 00:43:04 +08:00
MaysWind d3e1acddc5 code refactor, modify style 2023-07-30 00:26:32 +08:00
MaysWind a9c511eb2e fix the problem that cannot search the keywords which contains & symbol in the transaction list page 2023-07-29 23:02:34 +08:00
MaysWind ffef33a9fc code refactor, set category menu disabled when type is modify balance, modify style 2023-07-29 17:26:50 +08:00
MaysWind 982917ddbb set export data button disabled if no data can be exported 2023-07-29 15:39:50 +08:00
MaysWind 07406a50bb show account icon in user basic setting tab, always show default account name even if the account is hidden, set the submit button disabled when nothing has been changed 2023-07-29 15:35:55 +08:00
MaysWind 78f325e127 code refactor 2023-07-29 15:13:37 +08:00
MaysWind e1bb97a1db improve runtime caching pattern 2023-07-27 00:01:17 +08:00
MaysWind 831952806d hide hidden category in transaction list page 2023-07-26 23:37:43 +08:00
MaysWind 4c57b7a009 fix the border not show in some device 2023-07-26 23:33:01 +08:00
MaysWind 683188f67a code refactor 2023-07-24 23:36:04 +08:00
MaysWind 848e5271ab code refactor 2023-07-24 02:41:00 +08:00
MaysWind 7ca5614c44 remove unused code 2023-07-24 02:40:20 +08:00
MaysWind 70fc781a03 add transaction list page for desktop 2023-07-24 02:36:59 +08:00
MaysWind aafdbab781 fix the problem that the time zone setting did not take effect immediately 2023-07-24 02:23:19 +08:00
MaysWind 5dd0f7ea10 only add date time params to transaction list url when date range type is set to custom in transaction statistics page 2023-07-24 00:22:30 +08:00
MaysWind 9393d9105c add persistent props to date range selection dialog 2023-07-24 00:20:43 +08:00
MaysWind 2c3856be3c change url when switching tab in user/app settings page 2023-07-24 00:20:01 +08:00
MaysWind dc746a51a5 add set as baseline button to exchange rates page for desktop 2023-07-24 00:16:43 +08:00
MaysWind 0f2c9354f0 modify style and hide operation buttons when cursor not hovered on 2023-07-24 00:16:09 +08:00
MaysWind af00032ee9 check whether user is logined when entering every page 2023-07-23 12:00:18 +08:00
MaysWind 5d7e685dc4 use icon to replace symbol character 2023-07-23 01:32:55 +08:00
MaysWind bfcb79c02b modify style 2023-07-22 23:49:55 +08:00
MaysWind ebee6273b0 code refactor 2023-07-22 23:17:13 +08:00
MaysWind b45900e5bc modify text 2023-07-22 23:06:00 +08:00
MaysWind 9f0657683a modify style 2023-07-22 23:04:08 +08:00
MaysWind 35b8d8ca25 modify style 2023-07-21 00:47:13 +08:00
MaysWind 8f7095ce19 transaction tag list page supports dragging to change display order 2023-07-20 23:52:33 +08:00
MaysWind d9c8dd20e9 modify style 2023-07-20 23:29:26 +08:00
MaysWind 8dcc462a30 modify style 2023-07-20 23:16:00 +08:00
MaysWind b561948030 code refactor 2023-07-20 01:37:45 +08:00
MaysWind 2cbd8684cf account list page supports dragging to change display order 2023-07-19 02:32:10 +08:00
MaysWind 107c9fce94 fix default icon color 2023-07-19 00:24:00 +08:00
MaysWind 4c18b3e059 optimize vite build config 2023-07-18 23:52:49 +08:00
MaysWind 9960ec4d58 modify style 2023-07-18 23:15:39 +08:00
MaysWind 2a09e048e4 code refactor 2023-07-18 00:51:47 +08:00
MaysWind 9711f3ba72 modify style 2023-07-18 00:48:54 +08:00
MaysWind 0622d2b81b modify style 2023-07-18 00:46:37 +08:00
MaysWind a372d1fb60 code refactor 2023-07-18 00:41:26 +08:00
MaysWind 99e55e730e code refactor 2023-07-18 00:29:41 +08:00
MaysWind 57d1e915e6 remove unused function reference 2023-07-18 00:18:02 +08:00
MaysWind 96ad6228bd code refactor 2023-07-18 00:17:38 +08:00
MaysWind 678f9fbe87 fix wrong text 2023-07-17 23:14:35 +08:00
MaysWind 714933df56 code refactor 2023-07-17 23:13:54 +08:00
MaysWind f0ef9ad51e remove unused code 2023-07-17 23:05:48 +08:00
MaysWind 0255213934 code refactor 2023-07-17 23:02:51 +08:00
MaysWind 0ad92a999c modify style 2023-07-17 00:35:27 +08:00
MaysWind b06456d573 modify style 2023-07-17 00:13:57 +08:00
MaysWind 6f1fc2c9b4 modify field name 2023-07-17 00:00:12 +08:00
MaysWind 44a1982d87 code refactor 2023-07-16 23:57:18 +08:00
MaysWind 6b0cf5aa96 fix missing plus symbol issue when there are unsupported currencies of sub accounts 2023-07-16 23:56:56 +08:00
MaysWind 2cfac7a772 modify style 2023-07-16 23:48:04 +08:00
MaysWind 99aaf35e0b add account list page for desktop 2023-07-16 23:35:50 +08:00
MaysWind 942ed1fb55 code refactor 2023-07-16 23:12:44 +08:00
MaysWind 25f83a98e3 code refactor 2023-07-16 23:05:11 +08:00
MaysWind ed4040f2ec code refactor 2023-07-16 22:55:02 +08:00
MaysWind 41034de676 modify style 2023-07-16 18:35:30 +08:00
MaysWind 2db0f1a6c8 code refactor 2023-07-16 16:36:47 +08:00
MaysWind 6b06cc7ef5 desktop pie chart supports clicking the chart to scroll to the specified legend 2023-07-16 16:23:56 +08:00
MaysWind 2bf26212af modify transaction statistics page style 2023-07-16 15:28:43 +08:00
MaysWind 9d273c172d modify exchange rates page style 2023-07-16 15:19:44 +08:00
MaysWind 6ae3bc82bb modify style 2023-07-16 14:03:14 +08:00
MaysWind c782002274 modify style 2023-07-16 13:16:43 +08:00
MaysWind cd0906d041 update third party dependencies 2023-07-15 23:45:43 +08:00
MaysWind f936ecca33 manually get backend dependencies before lint checking 2023-07-15 23:44:48 +08:00
MaysWind 8794e3bc53 set confirm and more button disabled when there are no available items 2023-07-15 23:43:44 +08:00
MaysWind 4503e2a222 add necessary vuetify component and remove unnecessary vuetify component 2023-07-15 23:43:06 +08:00
MaysWind 015725f88c support setting account/transaction category filter for statistics page 2023-07-15 16:54:56 +08:00
MaysWind 39451f0e37 code refactor 2023-07-15 16:21:30 +08:00
MaysWind 2bb0b7faa5 modify text color 2023-07-15 16:07:41 +08:00
MaysWind 8db7c3769a remove blank line 2023-07-15 16:07:25 +08:00
MaysWind d4e32de882 item icon supports custom class 2023-07-15 01:38:34 +08:00
MaysWind db75dea9ee fix typo 2023-07-15 00:50:29 +08:00
MaysWind 489bba9c4b modify class name 2023-07-15 00:30:42 +08:00
MaysWind 4accc49d60 reload sessions after password changed 2023-07-15 00:24:47 +08:00
MaysWind 5a47cd5216 modify style 2023-07-14 23:50:59 +08:00
MaysWind 439608cf27 remove unused configuration 2023-07-14 22:46:00 +08:00
MaysWind f9df7a1b5a modify style 2023-07-11 01:21:45 +08:00
MaysWind 78c801994a fix issue after packing js file 2023-07-11 01:16:45 +08:00
MaysWind 882fd68cf9 add some transaction statistics settings 2023-07-11 01:01:11 +08:00
MaysWind 3433b73edf code refactor 2023-07-11 01:00:27 +08:00
MaysWind a235d6a8cd code refactor 2023-07-11 00:35:21 +08:00
MaysWind d76e88af21 code refactor 2023-07-11 00:30:57 +08:00
MaysWind 3f6c6c443a code refactor 2023-07-11 00:26:27 +08:00
MaysWind 13c6d406cf only navigate to transaction list page when click pie chart label in desktop version 2023-07-11 00:15:47 +08:00
MaysWind 19fea4e761 code refactor 2023-07-10 23:26:16 +08:00
MaysWind c84c96dcd1 update service worker config 2023-07-10 22:58:46 +08:00
MaysWind cdd8ccc2d4 modify image path 2023-07-10 22:46:52 +08:00
MaysWind 09210d5d40 remove switch to mobile/desktop version in unlock page 2023-07-10 00:38:22 +08:00
MaysWind 8f44a26037 code refactor 2023-07-10 00:14:33 +08:00
MaysWind 5e986b2d04 add transaction statistics page 2023-07-10 00:04:20 +08:00
MaysWind 298c0922cb fix account icon issue in transaction statistics data page 2023-07-09 21:21:19 +08:00
MaysWind dc127ea6a3 code refactor 2023-07-09 20:43:08 +08:00
MaysWind 522ed94c32 user settings and app settings page supports showing specified tab by query parameter 2023-07-09 16:35:12 +08:00
MaysWind 6edf66a599 code refactor 2023-07-09 14:22:47 +08:00
MaysWind 475ec24528 desktop page supports service worker 2023-07-09 13:20:24 +08:00
MaysWind b555af0df7 add lock application menu 2023-07-09 13:13:35 +08:00
MaysWind f90430e544 modify style 2023-07-09 12:45:40 +08:00
MaysWind 4ccb75818c add switch to mobile/desktop device in login/unlock page 2023-07-09 12:01:29 +08:00
MaysWind c5c9ed24c3 code refactor 2023-07-09 11:12:39 +08:00
MaysWind 386aa0adc1 code refactor 2023-07-09 01:07:10 +08:00
MaysWind 0b26b75699 modify style 2023-07-09 01:04:24 +08:00
MaysWind d013f67c70 itemicon supports hidden status 2023-07-09 00:43:42 +08:00
MaysWind dc7c0e61fd code refactor 2023-07-09 00:37:51 +08:00
MaysWind 89bd041d29 add transaction tag list page 2023-07-09 00:37:19 +08:00
MaysWind ac730d6086 remove number input stepper 2023-07-08 22:56:16 +08:00
MaysWind 427eaed544 code refactor 2023-07-08 20:31:41 +08:00
MaysWind 00f783c0b6 modify style 2023-07-08 20:28:15 +08:00
MaysWind 8e1e53d55e code refactor 2023-07-08 20:07:09 +08:00
MaysWind 5c9a5c13b8 modify style 2023-07-08 19:42:33 +08:00
MaysWind c1f03a8e75 modify style 2023-07-08 17:51:15 +08:00
MaysWind 1affa83620 code refactor 2023-07-08 16:42:35 +08:00
MaysWind 2aa8180113 code refactor 2023-07-08 16:16:28 +08:00
MaysWind e6001d83a4 modify style 2023-07-08 16:16:04 +08:00
MaysWind 8550c8fde9 modify style 2023-07-08 12:25:18 +08:00
MaysWind 48d9a09307 add sign up page 2023-07-08 02:59:50 +08:00
MaysWind 9d0b874488 code refactor 2023-07-07 23:48:32 +08:00
MaysWind 062a13b2c2 modify style 2023-07-07 22:53:22 +08:00
MaysWind 937f8723ed add background img for dark theme 2023-07-06 01:07:57 +08:00
MaysWind 0c5cabbd79 modify color 2023-07-06 00:56:10 +08:00
MaysWind 95d8f710d8 exclude unnecessary file from precaching files, add some files to runtime caching 2023-07-06 00:38:28 +08:00
MaysWind bb9b8b34e5 modify img path 2023-07-06 00:04:54 +08:00
MaysWind 54c1164bd7 add illustrations to login/unlock page 2023-07-05 01:27:05 +08:00
MaysWind 89c1158d95 modify style 2023-07-04 23:06:52 +08:00
MaysWind 9cae189941 modify text tip 2023-07-03 23:04:44 +08:00
MaysWind 53a31cd4c4 use variables to replace secrets 2023-07-03 23:02:17 +08:00
MaysWind d72a615481 code refactor 2023-07-03 23:01:31 +08:00
MaysWind 8dcffa80a8 exclude vendor files for desktop page 2023-07-02 23:50:57 +08:00
MaysWind 7cf53acd18 auto focus pin code input when open desktop unlock page 2023-07-02 23:38:15 +08:00
MaysWind 748cf68055 show avatar placeholder when loading user avatar 2023-07-02 23:34:09 +08:00
MaysWind 9cd22bdc06 add application lock setting tab for desktop 2023-07-02 23:22:59 +08:00
MaysWind 5830d4b91c desktop page support icon/startup image 2023-07-02 23:22:15 +08:00
MaysWind b42f7f566b add unlock page for desktop 2023-07-02 23:22:09 +08:00
MaysWind db8223ca98 fix the language not set to system language 2023-07-02 21:44:56 +08:00
MaysWind 9403afc392 code refactor 2023-07-02 21:44:37 +08:00
MaysWind 2b2d3b9517 code refactor 2023-07-02 20:01:59 +08:00
MaysWind 3eebdfcdb3 modify style 2023-07-02 19:31:47 +08:00
MaysWind f7766bc3d4 update Chinese translation 2023-07-02 19:28:53 +08:00
MaysWind f2614abbdd remove unused code 2023-07-02 19:25:30 +08:00
MaysWind 58824ea879 modify style 2023-07-02 19:09:40 +08:00
MaysWind 9ef7a18847 modify login page style 2023-07-02 19:07:55 +08:00
MaysWind 58c0167696 fix issue 2023-07-02 18:36:11 +08:00
MaysWind 9adfd286f9 code refactor 2023-07-02 00:51:26 +08:00
MaysWind 4e8f530fbb code refactor 2023-07-01 23:38:17 +08:00
MaysWind 3e694b0772 update Chinese translation 2023-07-01 23:27:09 +08:00
MaysWind 2fd5b04d4d code refactor 2023-07-01 23:26:31 +08:00
MaysWind 4688b9a9c9 move "Switch to Mobile Version" to "Use on Mobile Device" dialog 2023-07-01 23:16:53 +08:00
MaysWind 153e0035ba reset state after leaving 2fa setting tab 2023-07-01 23:10:49 +08:00
MaysWind 88b6ecc557 add alert component 2023-07-01 22:51:54 +08:00
MaysWind 9df55874a6 show username and avatar in user basic setting tab 2023-07-01 22:51:42 +08:00
MaysWind 87d4ab827a add reset button in user basic setting tab 2023-07-01 22:12:06 +08:00
MaysWind 318166f23a code refactor 2023-07-01 02:26:55 +08:00
MaysWind cc3f712659 remove unused code 2023-07-01 02:22:30 +08:00
MaysWind ee399d8a08 code refactor 2023-07-01 02:19:04 +08:00
MaysWind 96c233d5c5 code refactor 2023-07-01 02:05:36 +08:00
MaysWind 652b3e1954 support set gravatar as user avatar provider 2023-07-01 00:53:59 +08:00
MaysWind a8b76d5537 modify style 2023-07-01 00:15:17 +08:00
MaysWind e041b70cdd modify style 2023-07-01 00:09:15 +08:00
MaysWind 63bf912b3e upgrade vuetify to 3.3.6 2023-06-30 23:43:13 +08:00
MaysWind 0cbe7b1655 code refactor 2023-06-30 23:41:40 +08:00
MaysWind 7bbec29c5b show whether data is updated after click refresh button 2023-06-28 21:44:01 +08:00
MaysWind 7cec7dbac8 sort result in overview response 2023-06-28 21:41:59 +08:00
MaysWind 09a19b5f42 navigate to desktop page when use tablet device 2023-06-28 21:38:21 +08:00
MaysWind 8fe765c097 modify file name 2023-06-25 00:39:00 +08:00
MaysWind b11e8e07c4 set button disabled when required input is not filled 2023-06-25 00:30:48 +08:00
MaysWind f72763306d code refactor 2023-06-25 00:25:43 +08:00
MaysWind e3d1a476e2 fix wrong watching parameter 2023-06-24 23:59:53 +08:00
MaysWind f0bc86d42f add overview page 2023-06-24 23:55:52 +08:00
MaysWind a62e806175 fix wrong component reference 2023-06-24 21:51:39 +08:00
MaysWind 5bcf5bf93e show mobile url qrcode on desktop page 2023-06-24 21:28:28 +08:00
MaysWind 4f35ba0931 add settings page 2023-06-24 21:27:27 +08:00
MaysWind 2bcdfe778a code refactor 2023-06-24 21:27:19 +08:00
MaysWind c89da1d0f7 code refactor 2023-06-24 21:14:54 +08:00
MaysWind 46a1eda029 add page settings page 2023-06-24 20:01:47 +08:00
MaysWind 1c39819112 add switch to desktop/mobile link 2023-06-24 19:30:28 +08:00
MaysWind 0efe617c03 fix the problem that system default timezone is not browser timezone when custom timezone is set 2023-06-24 18:39:55 +08:00
MaysWind 10df947efe fix npe 2023-06-24 18:30:49 +08:00
MaysWind fb7790ba4a code refactor 2023-06-24 18:27:54 +08:00
MaysWind a9338ed822 code refactor 2023-06-24 14:15:15 +08:00
MaysWind eaab8cdd93 code refactor 2023-06-24 14:07:30 +08:00
MaysWind edfcd0dc6e use autocomplete to replace select 2023-06-24 01:22:35 +08:00
MaysWind 838b56089b remove blank line 2023-06-24 01:22:12 +08:00
MaysWind 178810f908 add user settings page 2023-06-24 01:18:41 +08:00
MaysWind 69f5aca853 fix autocomplete style issue 2023-06-24 00:16:19 +08:00
MaysWind 8e6aece9ae code refactor 2023-06-23 22:23:09 +08:00
MaysWind 59b883ff7f modify style 2023-06-23 21:16:17 +08:00
MaysWind 548d34fbf4 add tooltip 2023-06-23 18:00:31 +08:00
MaysWind bb6345ccfa update vue-datepicker, and make the picker show the calendar view every time opening 2023-06-23 15:48:47 +08:00
MaysWind d59a10f718 fix wrong style 2023-06-23 15:00:38 +08:00
MaysWind aab1c5419a remove unused reference 2023-06-23 14:53:22 +08:00
MaysWind 9b83785b95 modify style 2023-06-23 14:51:07 +08:00
MaysWind 099b710eb1 code refactor 2023-06-23 14:16:37 +08:00
MaysWind a5424afc38 modify style 2023-06-23 13:40:15 +08:00
MaysWind 626325066d code refactor 2023-06-23 13:38:09 +08:00
MaysWind 37195f6008 code refactor 2023-06-23 10:04:33 +08:00
MaysWind fcbc68cefe modify file name 2023-06-23 10:00:05 +08:00
MaysWind 9241953e31 modify style 2023-06-23 01:37:58 +08:00
MaysWind 651a912498 support change theme 2023-06-23 01:24:10 +08:00
MaysWind a05f6fb6b5 modify style 2023-06-23 00:59:14 +08:00
MaysWind d6440d31f2 add exchange rates page 2023-06-23 00:21:00 +08:00
MaysWind 88d5dc2f17 set default timeout setting 2023-06-22 22:54:48 +08:00
MaysWind a17ad85858 add about page 2023-06-22 21:48:19 +08:00
MaysWind 4b49c1f30f add desktop frontend framework 2023-06-22 21:30:18 +08:00
MaysWind a9e36b9a59 modify style 2023-06-22 18:43:39 +08:00
MaysWind 80429bbfb8 code refactor 2023-06-22 16:35:52 +08:00
MaysWind f39e20d7a7 support setting user disabled 2023-06-21 23:57:04 +08:00
MaysWind a03bac5d74 modify style 2023-06-21 23:38:37 +08:00
MaysWind 1aff09598a code refactor 2023-06-21 23:25:56 +08:00
MaysWind 4036a71ee1 update third party dependencies 2023-06-21 22:01:23 +08:00
MaysWind a966be2f46 package framework7 and related dependencies into vendor-mobile.js file 2023-06-21 21:52:31 +08:00
MaysWind a0b3bc1cab fix default text size 2023-06-21 21:50:16 +08:00
MaysWind eb2e2b0a26 show whether data is updated after pull down 2023-06-19 00:12:39 +08:00
MaysWind 55ad7b2e81 code refactor 2023-06-18 21:07:44 +08:00
MaysWind dbcd2897a4 support tomtom map 2023-06-18 21:02:55 +08:00
MaysWind 68a6d1c166 update configuration comment 2023-06-18 20:40:45 +08:00
MaysWind fe82ec6fc2 support OpenStreetMap(Humanitarian), OpenTopoMap, OPNVKarte, CyclOSM 2023-06-18 18:24:11 +08:00
MaysWind 5f2819a961 code refactor 2023-06-18 16:30:19 +08:00
MaysWind 812bfc7cf5 set reference libraries in google map js query 2023-06-18 16:29:30 +08:00
MaysWind d164cafd33 fix npe 2023-06-18 15:53:06 +08:00
MaysWind 3ada4183d9 code refactor 2023-06-18 15:51:57 +08:00
MaysWind fa68621b41 support api proxy for amap 2023-06-18 15:43:27 +08:00
MaysWind 4f2b9d39da code refactor 2023-06-18 09:20:02 +08:00
MaysWind fd01c9269f improve robustness 2023-06-18 01:25:47 +08:00
MaysWind 82d150e53a support amap 2023-06-18 01:24:29 +08:00
MaysWind 251f3fe2da update config comment 2023-06-18 00:26:18 +08:00
MaysWind 4f0e1e6b3d set position and zoom level when init map 2023-06-18 00:06:51 +08:00
MaysWind a5dbf5d4b7 support google map 2023-06-17 23:24:45 +08:00
MaysWind 38baf77c30 support get current language info 2023-06-17 23:07:22 +08:00
MaysWind bfb8b03fc9 add language code 2023-06-17 23:07:11 +08:00
MaysWind 2dd38d9b03 force set default language when specified language not exists, force set locale settings when first set locale settings 2023-06-17 23:04:46 +08:00
MaysWind 307c64bc1e don't write unnecessary info to cookies 2023-06-17 19:44:21 +08:00
MaysWind 782e3a85f9 support baidu map 2023-06-17 19:38:13 +08:00
MaysWind 3bae6e749a support baidu map 2023-06-17 17:47:12 +08:00
MaysWind 530ef6b83e fix default text size is not set to default 2023-06-17 13:09:39 +08:00
MaysWind 5b334eb2d5 show time difference between the transaction timezone and the default timezone on the transaction edit/view page 2023-06-15 01:31:29 +08:00
MaysWind 28dc2e425a fix the problem the end time not equals to the current time in transaction list page when timezone not set to browser timezone 2023-06-15 01:27:15 +08:00
mayswind 83b72e7403 remove duplicated code 2023-06-14 09:12:50 +08:00
MaysWind 01e1f65ffe display preview when drag the range slider 2023-06-14 01:38:52 +08:00
MaysWind 9b15f888e6 improve tree view selection sheet style 2023-06-14 01:36:27 +08:00
MaysWind 171b8afa8e improve text size settings 2023-06-14 01:27:23 +08:00
MaysWind 27f2f9f13a modify transaction edit page loading style 2023-06-14 00:45:49 +08:00
MaysWind 2c3983cead modify transaction list page loading style 2023-06-14 00:45:33 +08:00
MaysWind bebd043d58 add font settings page 2023-06-13 01:33:54 +08:00
MaysWind a1c828fe62 add more supported font size 2023-06-13 01:19:42 +08:00
MaysWind dfb885f38d support setting app font size 2023-06-12 01:39:23 +08:00
MaysWind 702c095544 fix the problem that the last label does not have divider line when filling in a new label 2023-06-11 22:55:12 +08:00
MaysWind b3e886d444 fix npe error 2023-06-11 22:53:32 +08:00
MaysWind 46d85e92cd use pinia to replace vuex, code refactor 2023-06-11 22:08:30 +08:00
MaysWind 0d84f2857f update README 2023-06-10 22:13:54 +08:00
MaysWind ac6c80db90 bump version to 0.4.0 2023-06-10 18:03:03 +08:00
MaysWind e608e85d56 support disable map 2023-06-10 17:14:58 +08:00
MaysWind 58104a9a4d append thousands separator in data management page 2023-06-10 17:04:35 +08:00
MaysWind 8d68dcabb5 list item selection sheet supports large size 2023-06-06 01:01:24 +08:00
MaysWind a5e5389d6c fix some text not changed when user language has been changed 2023-06-06 00:56:39 +08:00
MaysWind 8de862c82f optimize the style when the text is too long 2023-06-06 00:52:35 +08:00
MaysWind 5141303ee1 support turning on dark theme manually 2023-06-05 23:54:01 +08:00
MaysWind 3d95680cbc remove unused code 2023-06-05 09:47:09 +08:00
MaysWind 78663b873c support only match the first locale part when get browser language 2023-06-05 00:56:04 +08:00
MaysWind 0a106026dd user settings supports language and date&time format 2023-06-05 00:54:07 +08:00
MaysWind 999ca6274c remove unused file 2023-06-04 21:08:38 +08:00
MaysWind 36b9177069 fix building issue 2023-06-04 15:43:38 +08:00
MaysWind 8cf7bf859b support map provider and whether use map data proxy settings 2023-06-04 15:06:14 +08:00
MaysWind 2e54b62f60 set cookies in development mode 2023-06-04 14:25:51 +08:00
MaysWind df46069d92 code refactor 2023-06-04 13:15:09 +08:00
MaysWind e31014dde4 code refactor 2023-06-04 13:04:15 +08:00
MaysWind f9a14581e1 fix typo 2023-06-04 01:24:08 +08:00
MaysWind 95ec005894 add icons 2023-06-04 01:22:43 +08:00
MaysWind 73d271b8bc improve ui 2023-06-04 01:22:33 +08:00
MaysWind 0c3b56e44a set the color of input and confirm button to red in password input sheet when clear user data 2023-06-03 20:11:03 +08:00
MaysWind 736f340979 code refactor 2023-06-03 16:49:19 +08:00
MaysWind 49e62d35c3 exchange rate datasource supports Monetary Authority of Singapore 2023-05-29 01:04:16 +08:00
MaysWind 810bce7495 update latest available currencies 2023-05-29 00:27:30 +08:00
MaysWind aff876aa05 only trigger gitea docker-snapshot workflow when push to main branch 2023-05-28 17:59:25 +08:00
MaysWind 9511644ce6 update third party dependencies 2023-05-28 17:59:08 +08:00
MaysWind 21d73e5f69 update golang to 1.20.4, node to 18.16.0, base docker image to alpine 3.17.3 2023-05-28 12:18:40 +08:00
MaysWind d62f3fb936 update github actions 2023-05-21 00:06:11 +08:00
MaysWind 0a011b6075 add gitea actions 2023-05-20 23:49:00 +08:00
MaysWind 4e561dc764 change map marker icon 2023-05-15 23:31:53 +08:00
MaysWind a55256ad82 code refactor 2023-05-15 22:36:21 +08:00
MaysWind ab26ef64a5 code refactor 2023-05-15 22:35:59 +08:00
mayswind 6d1610eee0 fix wrong proxy path 2023-05-15 00:49:02 +08:00
MaysWind 2ba143d6ea support showing geolocation on map 2023-05-15 00:08:03 +08:00
MaysWind bd542ac308 modify geolocation storage type in database 2023-05-14 19:42:57 +08:00
MaysWind e71ffd1a77 support storing geo location in transaction 2023-04-29 13:45:19 +08:00
MaysWind 1ac968f63c improve ui 2023-04-28 17:21:27 +08:00
MaysWind dc4f62a085 modify Simplified Chinese translation 2023-04-28 16:26:52 +08:00
MaysWind ad91d4f1ce add show/hide hidden transaction category/tag menu 2023-04-28 16:24:15 +08:00
MaysWind 8d4c7512ab reduce unit test execute times 2023-04-24 00:54:24 +08:00
MaysWind 12d5837526 modify unit test case fail cause message 2023-04-23 21:49:27 +08:00
MaysWind c82d2abab4 fix problem that cannot switch to other date range sometimes in transaction list page 2023-04-23 01:27:38 +08:00
MaysWind 26cb717a08 build when push to non-main branch 2023-04-23 01:00:27 +08:00
MaysWind 0b1cc0ef5b add lint checking and unit testing in build script 2023-04-23 00:45:56 +08:00
MaysWind 05dc9138b4 fix problem that cannot get transaction statistics when db is set to only_full_group_by 2023-04-23 00:03:26 +08:00
MaysWind a7dcacb26c add log 2023-04-22 23:54:57 +08:00
MaysWind 3ac3f871e4 fix the problem that account list page does not update when modify category of account 2023-04-22 23:12:41 +08:00
MaysWind b180c0bbe6 modify link text 2023-04-22 22:35:36 +08:00
MaysWind e4987f3bde modify vue page name 2023-04-22 22:28:04 +08:00
MaysWind ee2690c2cc fix the problem that cannot change language in transaction preset category page 2023-04-22 21:50:08 +08:00
MaysWind 4cad26f793 change moment api 2023-04-22 21:26:42 +08:00
MaysWind 84f3d5fec5 remove unused code 2023-04-22 21:09:01 +08:00
MaysWind bb3939d570 show none when user does not have visible account or category 2023-04-22 21:03:41 +08:00
MaysWind b303f708f5 improve ui 2023-04-22 20:52:20 +08:00
MaysWind d39550e090 improve ui 2023-04-22 20:47:25 +08:00
MaysWind 22d956f38a improve ui 2023-04-22 20:24:53 +08:00
MaysWind dab7728138 update prompt text 2023-04-22 17:29:30 +08:00
MaysWind 4038b6bc51 improve ui 2023-04-22 17:00:56 +08:00
MaysWind bcaf3a246c show no available category / account in transaction statistics filter page when there are no available category or account 2023-04-22 16:19:10 +08:00
MaysWind 4fb115fcbc remove unused code 2023-04-22 01:37:16 +08:00
MaysWind bfd4b2b6de code refactor 2023-04-22 01:33:38 +08:00
MaysWind c0cd3fc5c2 improve ui 2023-04-22 01:24:49 +08:00
MaysWind f95c4393d2 fix the problem that the day of week in date time / date range picker is wrong 2023-04-22 01:10:22 +08:00
MaysWind e2eb5fabcc improve ui 2023-04-22 00:49:47 +08:00
MaysWind 7275e8ff0d modify infinite distance 2023-04-22 00:31:19 +08:00
MaysWind accfc3df12 improve ui 2023-04-21 23:56:52 +08:00
MaysWind 35392483d9 code refactor 2023-04-21 23:56:44 +08:00
MaysWind 9770851fd4 fix the problem that page jump infinitely 2023-04-21 23:56:02 +08:00
MaysWind e013a6f087 fix the display amount in statistics page when there are no transaction 2023-04-21 23:53:19 +08:00
MaysWind 1ec9ff20b1 remove unused code 2023-04-21 23:41:53 +08:00
MaysWind 9b0dea80c9 don't allow clear the value in datetime picker 2023-04-21 23:14:26 +08:00
MaysWind eea1ea7ed0 always show date range picker in center 2023-04-21 22:36:57 +08:00
MaysWind 85cd46bfc7 fix problem the category separate icon in transaction page does not display 2023-04-21 22:30:07 +08:00
MaysWind a7ca394864 code refactor 2023-04-21 22:20:11 +08:00
MaysWind e178a0795a code refactor 2023-04-21 22:16:35 +08:00
MaysWind e8b0470ceb code refactor 2023-04-21 22:11:10 +08:00
MaysWind c08abfdfdf fix frontend build issue 2023-04-21 08:47:15 +08:00
mayswind b1c765eb51 Upgrade to vue3 (#16)
* upgrade to vue 3.x and framework7 8.x
* change calendar plugin to vue-datepicker
* disable export button when user does not hava any transaction
* implement new pin code input
* append thousands separator in amount in exchange rates page
2023-04-21 01:45:00 +08:00
mayswind 4b0f7d45e8 Merge pull request #15 from vigdail/bugfix/atomic_alignment
Proper fields alignment in `InternalUuidGenerator` struct
2023-04-20 22:49:49 +08:00
vigdail 9e6271b1dc Rearrange fields of InternalUuidGenerator struct to fit atomic alignment requirements 2023-04-20 19:39:38 +06:00
MaysWind a96eb31dc9 redirect to login page when user logout without token 2023-04-16 02:00:59 +08:00
MaysWind 221a7809b6 fix npe error 2023-04-12 00:24:20 +08:00
MaysWind 8c33243c90 code refactor 2023-04-09 01:04:00 +08:00
MaysWind c4b07b98aa update third party dependencies 2023-04-08 14:53:26 +08:00
MaysWind 1fda80a86b bump version to 0.3.0 2023-04-08 14:49:39 +08:00
MaysWind 1287c729f2 modify font color style in transaction view page 2023-04-03 00:29:25 +08:00
MaysWind c069faa6f4 modify item header color in ios dark theme 2023-04-03 00:25:50 +08:00
MaysWind 286fd91b2b modify setting ui 2023-04-02 23:29:41 +08:00
MaysWind 33250d2f3d optimize user data export process 2023-04-02 23:18:05 +08:00
MaysWind 44ca940ca3 add splash screen images for ios 2023-04-02 21:05:59 +08:00
MaysWind 3b0ef7a96d data management page shows all user data statistics 2023-04-02 19:36:10 +08:00
MaysWind dfb6c593e4 format code 2023-04-02 18:14:16 +08:00
MaysWind 853b01e2ca show message when force update exchange rates data and the data is up to date 2023-04-02 18:06:36 +08:00
MaysWind 5a924fa382 code refactor 2023-04-02 17:35:31 +08:00
MaysWind d4985a024d use unambiguous numeric variable type 2023-03-27 23:58:33 +08:00
MaysWind 2797266de6 check numeric setting value, add numeric value range comment in config file 2023-03-27 23:13:18 +08:00
MaysWind b476cc91ca bump year 2023-03-27 22:10:57 +08:00
MaysWind 9e3aa19a09 make related transaction has the same unix time with the original transaction 2023-03-27 00:11:39 +08:00
MaysWind 7443e8a532 uuid generator supports generating more than 1 uuid in one time 2023-03-27 00:02:48 +08:00
MaysWind 1d6dbf63c0 code refactor 2023-03-26 23:53:52 +08:00
MaysWind e17687f80d fix wrong string format placeholder 2023-03-26 23:24:29 +08:00
MaysWind 27f4e14a4e code refactor 2023-03-26 22:24:26 +08:00
MaysWind 8d5de98218 record transaction created ip 2023-03-26 22:10:04 +08:00
MaysWind dbf5c0a5bd update golang to 1.20 and update nodejs to 18.15 2023-03-24 00:45:33 +08:00
MaysWind c1422f789a update third party dependencies 2023-03-24 00:40:46 +08:00
MaysWind 613af9399a update badge image 2023-01-08 21:57:22 +08:00
MaysWind 34a6108bb2 modify the end time of last 7/30 days to the end time in today 2023-01-05 22:34:45 +08:00
MaysWind 66a5508abe update third party dependencies 2022-12-06 01:16:02 +08:00
MaysWind 3a273ea64f update base image 2022-12-06 01:14:39 +08:00
MaysWind 6f88e6ef26 add health check api 2022-12-05 22:59:26 +08:00
MaysWind fd7905833e optimize statistics page style 2022-08-01 00:33:19 +08:00
MaysWind 1977e436d6 add "sort by" drop down list in statistics page 2022-07-25 01:11:35 +08:00
MaysWind 0dfb3d00e9 code refactor 2022-07-25 00:22:04 +08:00
MaysWind 785ec9bdb1 update docker base image 2022-07-24 20:35:55 +08:00
MaysWind eeccc4fd49 update third party dependencies 2022-07-24 20:14:39 +08:00
MaysWind efea9a7c37 update year 2022-07-24 19:20:40 +08:00
MaysWind 04eafd6705 code refactor 2022-07-24 19:00:16 +08:00
MaysWind 73b554aa48 auto set transaction type in transaction adding page according to the category id 2022-07-24 17:56:49 +08:00
MaysWind d3ddcf4c20 bump version to 0.2.0 2022-07-24 15:45:12 +08:00
MaysWind ed9ea2f1d3 modify cancel button text and position in license popup 2022-07-21 01:10:36 +08:00
MaysWind 7eae3e9923 show currency code when show currency name 2022-07-21 01:07:43 +08:00
MaysWind 03d95033d7 allow set base amount in exchange rate page 2022-07-21 00:41:37 +08:00
MaysWind 9a79606565 support set user default account 2022-04-18 00:16:47 +08:00
MaysWind c5a101aad2 fix bug 2022-04-17 23:32:00 +08:00
MaysWind 2dbf4d652d update third party dependencies 2022-03-20 23:20:11 +08:00
MaysWind 7364380312 support filter by parent account in transaction list page 2022-03-20 22:26:27 +08:00
MaysWind b7fe70aba3 change method name 2022-03-07 00:27:12 +08:00
MaysWind bca9982c57 hide add icon when filter parent account in transaction list page 2022-03-07 00:02:00 +08:00
MaysWind 7c16435010 make parent account clickable in account list page 2022-03-06 23:55:18 +08:00
MaysWind a79def625c update third party dependencies 2022-03-06 22:59:24 +08:00
MaysWind 8d5f804a60 modify save button text 2022-01-03 20:25:56 +08:00
mayswind 0167381f0d Merge pull request #1 from jiangshengwu/fix_transaction_amount
remove uid in selected columns
2021-07-12 20:31:15 +08:00
Shengwu Jiang 4e2c4b39bb remove uid in selected columns 2021-07-12 20:18:00 +08:00
MaysWind 7bfc84abc8 update comment 2021-07-04 22:57:28 +08:00
MaysWind 5ac6c64079 add data & log folder for building package 2021-06-29 23:57:21 +08:00
MaysWind e7c4261b86 add generating secret key utility 2021-06-28 00:41:14 +08:00
MaysWind 163b75e81b modify method name 2021-06-28 00:39:12 +08:00
MaysWind 949132ef5a modify timezone 2021-06-27 23:15:10 +08:00
MaysWind 5617d31ed8 update build.sh help message 2021-06-23 00:13:17 +08:00
MaysWind 832d865397 modify comments in config file 2021-06-21 00:54:14 +08:00
437 changed files with 55702 additions and 28591 deletions
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
'root': true,
'env': {
'node': true
},
'extends': [
'eslint:recommended',
'plugin:vue/vue3-essential'
],
'rules': {
'vue/no-use-v-if-with-v-for': 'off'
}
}
+55
View File
@@ -0,0 +1,55 @@
name: Docker Release
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set up the environment
run: |
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_RELEASE_PLATFORMS }}
push: true
build-args: |
RELEASE_BUILD=1
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+54
View File
@@ -0,0 +1,54 @@
name: Docker Snapshot
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
type=sha,format=short,prefix=SNAPSHOT-
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set up the environment
run: |
sed -i 's#FROM #FROM ${{ secrets.DOCKER_REPO }}/mirrors/#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+32 -23
View File
@@ -9,35 +9,44 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up docker tag
id: vars
run: echo ::set-output name=RELEASE_TAG::${GITHUB_REF/refs\/tags\/v/}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
platforms: |
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/arm/v6
push: true
build-args: |
RELEASE_BUILD=1
tags: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:${{ steps.vars.outputs.RELEASE_TAG }}
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:latest
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+31 -23
View File
@@ -9,33 +9,41 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up docker tag
id: vars
run: echo ::set-output name=BUILD_DATE::$(date '+%Y%m%d')
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
platforms: |
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/arm/v6
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:SNAPSHOT-${{ steps.vars.outputs.BUILD_DATE }}
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:latest-snapshot
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -0,0 +1,28 @@
name: Docker Snapshot
on:
push:
branches-ignore:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build
uses: docker/build-push-action@v2
with:
file: Dockerfile
context: .
platforms: linux/amd64
push: false
+4 -3
View File
@@ -1,5 +1,5 @@
# Build backend binary file
FROM golang:1.16.5-alpine3.13 AS be-builder
FROM golang:1.20.8-alpine3.17 AS be-builder
ARG RELEASE_BUILD
ENV RELEASE_BUILD=$RELEASE_BUILD
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
@@ -9,7 +9,7 @@ RUN apk add git gcc g++ libc-dev
RUN ./build.sh backend
# Build frontend files
FROM node:14.17.0-alpine3.13 AS fe-builder
FROM node:18.17.1-alpine3.17 AS fe-builder
ARG RELEASE_BUILD
ENV RELEASE_BUILD=$RELEASE_BUILD
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
@@ -19,7 +19,7 @@ RUN apk add git
RUN ./build.sh frontend
# Package docker image
FROM alpine:3.13.5
FROM alpine:3.17.5
LABEL maintainer="MaysWind <i@mayswind.net>"
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
RUN apk --no-cache add tzdata
@@ -32,6 +32,7 @@ WORKDIR /ezbookkeeping
COPY --from=be-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/ezbookkeeping /ezbookkeeping/ezbookkeeping
COPY --from=fe-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/dist /ezbookkeeping/public
COPY --chown=1000:1000 conf /ezbookkeeping/conf
COPY --chown=1000:1000 templates /ezbookkeeping/templates
COPY --chown=1000:1000 LICENSE /ezbookkeeping/LICENSE
USER 1000:1000
EXPOSE 8080
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2021 MaysWind (i@mayswind.net)
Copyright (c) 2020-2023 MaysWind (i@mayswind.net)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+11 -6
View File
@@ -1,6 +1,6 @@
# ezBookkeeping
[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
[![Latest Build](https://img.shields.io/github/workflow/status/mayswind/ezbookkeeping/Docker%20Release?style=flat)](https://github.com/mayswind/ezbookkeeping/actions)
[![Latest Build](https://img.shields.io/github/actions/workflow/status/mayswind/ezbookkeeping/docker-snapshot.yml?branch=main)](https://github.com/mayswind/ezbookkeeping/actions)
[![Go Report](https://goreportcard.com/badge/github.com/mayswind/ezbookkeeping)](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
[![Latest Docker Image Size](https://img.shields.io/docker/image-size/mayswind/ezbookkeeping.svg?style=flat)](https://hub.docker.com/r/mayswind/ezbookkeeping)
[![Latest Release](https://img.shields.io/github/release/mayswind/ezbookkeeping.svg?style=flat)](https://github.com/mayswind/ezbookkeeping/releases)
@@ -13,12 +13,14 @@ ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It c
2. Lightweight & Fast
3. Easy to install
* Docker support
* Multiple database support (sqlite, mysql, etc.)
* Multiple os & architecture support (Windows, macOS, Linux & x86, amd64, ARM)
* Multiple database support (SQLite, MySQL, PostgreSQL, etc.)
* Multiple operation system & hardware support (Windows, macOS, Linux & x86, amd64, ARM)
4. User-friendly interface
* Both desktop and mobile UI
* Close to native app experience (for mobile device)
* Two-level account & two-level category support
* Plentiful preset categories
* Geographic location and map support
* Searching & filtering history records
* Data statistics
* Dark theme
@@ -26,12 +28,15 @@ ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It c
6. Multiple timezone support
7. Multi-language support
8. Two-factor authentication
9. Application lock (WebAuthn support)
9. Application lock (PIN code / WebAuthn)
10. Data export
## Screenshots
### Mobile Device
[![ezBookkeeping](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/en.png)](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/en.png)
### Desktop Version
[![ezBookkeeping](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/desktop/en.png)](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/desktop/en.png)
### Mobile Version
[![ezBookkeeping](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/en.png)](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/en.png)
## Installation
### Ship with docker
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+60 -15
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env sh
TYPE=""
NO_LINT="0"
NO_TEST="0"
RELEASE=${RELEASE_BUILD:-"0"}
RELEASE_TYPE="unknown"
VERSION=""
@@ -31,16 +33,18 @@ Usage:
build.sh type [options]
Types:
backend Build backend binary file
frontend Build frontend files
package Build package archive
docker Build docker image
backend Build backend binary file
frontend Build frontend files
package Build package archive
docker Build docker image
Options:
-r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
-o, --output Package file name (For "package" type only)
-t, --tag Docker tag (For "docker" type only)
-h, --help Show help
-r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
-o, --output <filename> Package file name (For "package" type only)
-t, --tag Docker tag (For "docker" type only)
--no-lint Do not execute lint check before building
--no-test Do not execute unit testing before building
-h, --help Show help
EOF
}
@@ -63,6 +67,12 @@ parse_args() {
DOCKER_TAG="$2"
shift
;;
--no-lint)
NO_LINT="1"
;;
--no-test)
NO_TEST="1"
;;
--help | -h)
show_help
exit 0
@@ -111,6 +121,30 @@ set_build_parameters() {
}
build_backend() {
echo "Pulling backend dependencies..."
go get .
if [ "$NO_LINT" = "0" ]; then
echo "Executing backend lint checking..."
go vet -v ./...
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass lint checking"
exit 1
fi
fi
if [ "$NO_TEST" = "0" ]; then
echo "Executing backend unit testing..."
go clean -cache
go test ./... -v
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass unit testing"
exit 1
fi
fi
backend_build_extra_arguments="-X main.Version=$VERSION"
backend_build_extra_arguments="$backend_build_extra_arguments -X main.CommitHash=$COMMIT_HASH"
@@ -125,17 +159,26 @@ build_backend() {
}
build_frontend() {
frontend_build_arguments="";
if [ "$RELEASE" = "0" ]; then
frontend_build_arguments="--buildUnixTime=$BUILD_UNIXTIME"
fi
echo "Pulling frontend dependencies..."
npm install
if [ "$NO_LINT" = "0" ]; then
echo "Executing frontend lint checking..."
npm run lint
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass lint checking"
exit 1
fi
fi
echo "Building frontend files ($RELEASE_TYPE)..."
npm run build -- "$frontend_build_arguments"
if [ "$RELEASE" = "0" ]; then
buildUnixTime=$BUILD_UNIXTIME npm run build
else
npm run build
fi
}
build_package() {
@@ -158,6 +201,8 @@ build_package() {
rm -rf package
mkdir package
mkdir package/data
mkdir package/log
cp ezbookkeeping package/
cp -R dist package/public
cp -R conf package/conf
+44 -11
View File
@@ -9,6 +9,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
@@ -17,29 +18,40 @@ import (
func initializeSystem(c *cli.Context) (*settings.Config, error) {
var err error
configFilePath := c.String("conf-path")
isDisableBootLog := c.Bool("no-boot-log")
if configFilePath != "" {
if _, err = os.Stat(configFilePath); err != nil {
log.BootErrorf("[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath)
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath)
}
return nil, err
}
log.BootInfof("[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath)
if !isDisableBootLog {
log.BootInfof("[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath)
}
} else {
configFilePath, err = settings.GetDefaultConfigFilePath()
if err != nil {
log.BootErrorf("[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error())
}
return nil, err
}
log.BootInfof("[initializer.initializeSystem] will load configuration from default config path %s", configFilePath)
if !isDisableBootLog {
log.BootInfof("[initializer.initializeSystem] will load configuration from default config path %s", configFilePath)
}
}
config, err := settings.LoadConfiguration(configFilePath)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] cannot load configuration, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] cannot load configuration, because %s", err.Error())
}
return nil, err
}
@@ -48,33 +60,53 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
err = datastore.InitializeDataStore(config)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] initializes data store failed, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes data store failed, because %s", err.Error())
}
return nil, err
}
err = log.SetLoggerConfiguration(config)
err = log.SetLoggerConfiguration(config, isDisableBootLog)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error())
}
return nil, err
}
err = uuid.InitializeUuidGenerator(config)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error())
}
return nil, err
}
err = mail.InitializeMailer(config)
if err != nil {
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes mailer failed, because %s", err.Error())
}
return nil, err
}
err = exchangerates.InitializeExchangeRatesDataSource(config)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] initializes exchange rates data source failed, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes exchange rates data source failed, because %s", err.Error())
}
return nil, err
}
cfgJson, _ := json.Marshal(getConfigWithoutSensitiveData(config))
log.BootInfof("[initializer.initializeSystem] has loaded configuration %s", cfgJson)
if !isDisableBootLog {
log.BootInfof("[initializer.initializeSystem] has loaded configuration %s", cfgJson)
}
return config, nil
}
@@ -88,6 +120,7 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
}
clonedConfig.DatabaseConfig.DatabasePassword = "****"
clonedConfig.SMTPConfig.SMTPPasswd = "****"
clonedConfig.SecretKey = "****"
return clonedConfig
+49
View File
@@ -0,0 +1,49 @@
package cmd
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// SecurityUtils represents the security command
var SecurityUtils = &cli.Command{
Name: "security",
Usage: "ezBookkeeping security utilities",
Subcommands: []*cli.Command{
{
Name: "gen-secret-key",
Usage: "Generate a random secret key",
Action: genSecretKey,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "length",
Aliases: []string{"l"},
Required: false,
DefaultText: "32",
Usage: "The length of secret key",
},
},
},
},
}
func genSecretKey(c *cli.Context) error {
length := c.Int("length")
if length <= 0 {
length = 32
}
secretKey, err := utils.GetRandomNumberOrLetter(length)
if err != nil {
return err
}
fmt.Printf("[Secret Key] %s\n", secretKey)
return nil
}
+206 -2
View File
@@ -86,6 +86,71 @@ var UserData = &cli.Command{
},
},
},
{
Name: "user-enable",
Usage: "Enable specified user",
Action: enableUser,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-disable",
Usage: "Disable specified user",
Action: disableUser,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-resend-verify-email",
Usage: "Resend user verify email",
Action: resendUserVerifyEmail,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-set-email-verified",
Usage: "Set user email address verified",
Action: setUserEmailVerified,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-set-email-unverified",
Usage: "Set user email address unverified",
Action: setUserEmailUnverified,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-delete",
Usage: "Delete specified user",
@@ -138,6 +203,19 @@ var UserData = &cli.Command{
},
},
},
{
Name: "send-password-reset-mail",
Usage: "Send password reset mail",
Action: sendPasswordResetMail,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "transaction-check",
Usage: "Check whether user all transactions and accounts are correct",
@@ -239,6 +317,126 @@ func modifyUserPassword(c *cli.Context) error {
return nil
}
func sendPasswordResetMail(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.SendPasswordResetMail(c, username)
if err != nil {
log.BootErrorf("[user_data.sendPasswordResetMail] error occurs when sending password reset email")
return err
}
log.BootInfof("[user_data.sendPasswordResetMail] a password reset email for user \"%s\" has been sent", username)
return nil
}
func enableUser(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.EnableUser(c, username)
if err != nil {
log.BootErrorf("[user_data.enableUser] error occurs when setting user enabled")
return err
}
log.BootInfof("[user_data.enableUser] user \"%s\" has been set enabled", username)
return nil
}
func disableUser(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.DisableUser(c, username)
if err != nil {
log.BootErrorf("[user_data.disableUser] error occurs when setting user disabled")
return err
}
log.BootInfof("[user_data.disableUser] user \"%s\" has been set disabled", username)
return nil
}
func resendUserVerifyEmail(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.ResendVerifyEmail(c, username)
if err != nil {
log.BootErrorf("[user_data.resendUserVerifyEmail] error occurs when resending user verify email")
return err
}
log.BootInfof("[user_data.resendUserVerifyEmail] verify email for user \"%s\" has been resent", username)
return nil
}
func setUserEmailVerified(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.SetUserEmailVerified(c, username)
if err != nil {
log.BootErrorf("[user_data.setUserEmailVerified] error occurs when setting user email address verified")
return err
}
log.BootInfof("[user_data.setUserEmailVerified] user \"%s\" email address has been set verified", username)
return nil
}
func setUserEmailUnverified(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.SetUserEmailUnverified(c, username)
if err != nil {
log.BootErrorf("[user_data.setUserEmailUnverified] error occurs when setting user email address unverified")
return err
}
log.BootInfof("[user_data.setUserEmailUnverified] user \"%s\" email address has been set unverified", username)
return nil
}
func deleteUser(c *cli.Context) error {
_, err := initializeSystem(c)
@@ -398,9 +596,15 @@ func printUserInfo(user *models.User) {
fmt.Printf("[Nickname] %s\n", user.Nickname)
fmt.Printf("[Password] %s\n", user.Password)
fmt.Printf("[Salt] %s\n", user.Salt)
fmt.Printf("[DefaultAccountId] %d\n", user.DefaultAccountId)
fmt.Printf("[TransactionEditScope] %s (%d)\n", user.TransactionEditScope, user.TransactionEditScope)
fmt.Printf("[Language] %s\n", user.Language)
fmt.Printf("[DefaultCurrency] %s\n", user.DefaultCurrency)
fmt.Printf("[FirstDayOfWeek] %s\n", user.FirstDayOfWeek)
fmt.Printf("[TransactionEditScope] %s\n", user.TransactionEditScope)
fmt.Printf("[FirstDayOfWeek] %s (%d)\n", user.FirstDayOfWeek, user.FirstDayOfWeek)
fmt.Printf("[LongDateFormat] %s (%d)\n", user.LongDateFormat, user.LongDateFormat)
fmt.Printf("[ShortDateFormat] %s (%d)\n", user.ShortDateFormat, user.ShortDateFormat)
fmt.Printf("[LongTimeFormat] %s (%d)\n", user.LongTimeFormat, user.LongTimeFormat)
fmt.Printf("[ShortTimeFormat] %s (%d)\n", user.ShortTimeFormat, user.ShortTimeFormat)
fmt.Printf("[Deleted] %t\n", user.Deleted)
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
+128
View File
@@ -0,0 +1,128 @@
package cmd
import (
"encoding/binary"
"fmt"
"net"
"github.com/urfave/cli/v2"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/requestid"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// Utilities represents the utilities command
var Utilities = &cli.Command{
Name: "utility",
Usage: "ezBookkeeping utilities",
Subcommands: []*cli.Command{
{
Name: "parse-default-request-id",
Usage: "Parse a request id which is generated by default request generator and show the details",
Action: parseRequestId,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "id",
Required: true,
Usage: "Request ID",
},
},
},
{
Name: "send-test-mail",
Usage: "Send an email to specified e-mail address",
Action: sendTestMail,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "to",
Required: true,
Usage: "To e-mail address",
},
},
},
},
}
func parseRequestId(c *cli.Context) error {
config, err := initializeSystem(c)
if err != nil {
return err
}
err = requestid.InitializeRequestIdGenerator(config)
defaultGenerator, err := requestid.NewDefaultRequestIdGenerator(config)
if err != nil {
return err
}
requestId := c.String("id")
requestIdInfo, err := defaultGenerator.ParseRequestIdInfo(requestId)
if err != nil {
return err
}
newRequestId := defaultGenerator.GenerateRequestId(net.IPv4zero.String())
newRequestIdInfo, err := defaultGenerator.ParseRequestIdInfo(newRequestId)
printRequestIdInfo(requestId, requestIdInfo, newRequestIdInfo)
return nil
}
func sendTestMail(c *cli.Context) error {
config, err := initializeSystem(c)
if err != nil {
return err
}
if !config.EnableSMTP || mail.Container.Current == nil {
return errs.ErrSMTPServerNotEnabled
}
toAddress := c.String("to")
err = mail.Container.Current.SendMail(&mail.MailMessage{
To: toAddress,
Subject: "ezBookkeeping test e-mail",
Body: "This is a test e-mail",
})
if err != nil {
return err
}
fmt.Printf("Test e-mail has been sent")
return nil
}
func printRequestIdInfo(requestId string, requestIdInfo *requestid.RequestIdInfo, newRequestIdInfo *requestid.RequestIdInfo) {
fmt.Printf("[RequestId] %s\n", requestId)
fmt.Printf("[ServerUniqId] %d (Current Server %d)\n", requestIdInfo.ServerUniqId, newRequestIdInfo.ServerUniqId)
fmt.Printf("[InstanceUniqId] %d (Current Server %d)\n", requestIdInfo.InstanceUniqId, newRequestIdInfo.InstanceUniqId)
displayTime, err := utils.ParseFromElapsedSeconds(int(requestIdInfo.SecondsElapsedToday))
if err == nil {
fmt.Printf("[SecondsElapsedToday] %d (%s)\n", requestIdInfo.SecondsElapsedToday, displayTime)
} else {
fmt.Printf("[SecondsElapsedToday] %d\n", requestIdInfo.SecondsElapsedToday)
}
fmt.Printf("[RandomNumber] %d\n", requestIdInfo.RandomNumber)
fmt.Printf("[RequestSeqId] %d\n", requestIdInfo.RequestSeqId)
fmt.Printf("[IsClientIpv6] %t\n", requestIdInfo.IsClientIpv6)
if requestIdInfo.IsClientIpv6 {
fmt.Printf("[ClientIpv6Hash] %d\n", requestIdInfo.ClientIp)
} else {
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, requestIdInfo.ClientIp)
fmt.Printf("[ClientIpv4] %s\n", ip.String())
}
}
+127 -14
View File
@@ -3,7 +3,10 @@ package cmd
import (
"fmt"
"path/filepath"
"time"
"github.com/gin-contrib/cache"
"github.com/gin-contrib/cache/persistence"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
@@ -71,6 +74,8 @@ func startWebServer(c *cli.Context) error {
gin.SetMode(gin.ReleaseMode)
}
workboxFileNames := utils.ListFileNamesWithPrefixAndSuffix(config.StaticRootPath, "workbox-", ".js")
router := gin.New()
router.Use(bindMiddleware(middlewares.Recovery))
@@ -110,13 +115,16 @@ func startWebServer(c *cli.Context) error {
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
router.Static("/mobile/fonts", filepath.Join(config.StaticRootPath, "fonts"))
router.Static("/mobile/sw", filepath.Join(config.StaticRootPath, "sw"))
router.StaticFile("/mobile/favicon.ico", filepath.Join(config.StaticRootPath, "favicon.ico"))
router.StaticFile("/mobile/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
router.StaticFile("/mobile/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
for i := 0; i < len(workboxFileNames); i++ {
router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
desktopEntryRoute := router.Group("/desktop")
desktopEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
{
@@ -130,37 +138,90 @@ func startWebServer(c *cli.Context) error {
router.StaticFile("/desktop/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
router.StaticFile("/desktop/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
router.StaticFile("/desktop/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
router.StaticFile("/desktop/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
for i := 0; i < len(workboxFileNames); i++ {
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
if config.Mode == settings.MODE_DEVELOPMENT {
devRoute := router.Group("/dev")
devRoute.GET("/cookies", bindMiddleware(middlewares.ServerSettingsCookie(config)))
}
proxyRoute := router.Group("/proxy")
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
if config.EnableMapDataFetchProxy {
if config.MapProvider == settings.OpenStreetMapProvider ||
config.MapProvider == settings.OpenStreetMapHumanitarianStyleProvider ||
config.MapProvider == settings.OpenTopoMapProvider ||
config.MapProvider == settings.OPNVKarteMapProvider ||
config.MapProvider == settings.CyclOSMMapProvider ||
config.MapProvider == settings.TomTomMapProvider {
proxyRoute.GET("/map/tile/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapTileImageProxyHandler))
}
}
}
if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
amapApiProxyRoute := router.Group("/_AMapService")
amapApiProxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByCookie))
{
amapApiProxyRoute.GET("/*action", bindProxy(api.AmapApis.AmapApiProxyHandler))
}
}
qrCodeRoute := router.Group("/qrcode")
qrCodeRoute.Use(bindMiddleware(middlewares.RequestId(config)))
{
qrCodeCacheStore := persistence.NewInMemoryStore(time.Minute)
qrCodeRoute.GET("/mobile_url.png", bindCachedPngImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore))
}
apiRoute := router.Group("/api")
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
apiRoute.Use(bindMiddleware(middlewares.RequestLog))
{
apiRoute.POST("/authorize.json", bindApi(api.Authorizations.AuthorizeHandler))
apiRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.AuthorizeHandler, config))
if config.EnableTwoFactor {
twoFactorRoute := apiRoute.Group("/2fa")
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization))
{
twoFactorRoute.POST("/authorize.json", bindApi(api.Authorizations.TwoFactorAuthorizeHandler))
twoFactorRoute.POST("/recovery.json", bindApi(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler))
twoFactorRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeHandler, config))
twoFactorRoute.POST("/recovery.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler, config))
}
}
if config.EnableUserRegister {
apiRoute.POST("/register.json", bindApi(api.Users.UserRegisterHandler))
apiRoute.POST("/register.json", bindApiWithTokenUpdate(api.Users.UserRegisterHandler, config))
}
if config.EnableDataExport {
dataRoute := apiRoute.Group("/data")
dataRoute.Use(bindMiddleware(middlewares.HeaderInQueryString))
dataRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
if config.EnableUserVerifyEmail {
apiRoute.POST("/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByUnloginUserHandler))
emailVerifyRoute := apiRoute.Group("/verify_email")
emailVerifyRoute.Use(bindMiddleware(middlewares.JWTEmailVerifyAuthorization))
{
dataRoute.GET("/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
emailVerifyRoute.POST("/by_token.json", bindApi(api.Users.UserEmailVerifyHandler))
}
}
apiRoute.GET("/logout.json", bindApi(api.Tokens.TokenRevokeCurrentHandler))
if config.EnableUserForgetPassword {
apiRoute.POST("/forget_password/request.json", bindApi(api.ForgetPasswords.UserForgetPasswordRequestHandler))
resetPasswordRoute := apiRoute.Group("/forget_password/reset")
resetPasswordRoute.Use(bindMiddleware(middlewares.JWTResetPasswordAuthorization))
{
resetPasswordRoute.POST("/by_token.json", bindApi(api.ForgetPasswords.UserResetPasswordHandler))
}
}
apiRoute.GET("/logout.json", bindApiWithTokenUpdate(api.Tokens.TokenRevokeCurrentHandler, config))
apiV1Route := apiRoute.Group("/v1")
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization))
@@ -169,24 +230,33 @@ func startWebServer(c *cli.Context) error {
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
apiV1Route.POST("/tokens/revoke_all.json", bindApi(api.Tokens.TokenRevokeAllHandler))
apiV1Route.POST("/tokens/refresh.json", bindApi(api.Tokens.TokenRefreshHandler))
apiV1Route.POST("/tokens/refresh.json", bindApiWithTokenUpdate(api.Tokens.TokenRefreshHandler, config))
// Users
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
apiV1Route.POST("/users/profile/update.json", bindApi(api.Users.UserUpdateProfileHandler))
apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config))
if config.EnableUserVerifyEmail {
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
}
// Two Factor Authorization
if config.EnableTwoFactor {
apiV1Route.GET("/users/2fa/status.json", bindApi(api.TwoFactorAuthorizations.TwoFactorStatusHandler))
apiV1Route.POST("/users/2fa/enable/request.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableRequestHandler))
apiV1Route.POST("/users/2fa/enable/confirm.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableConfirmHandler))
apiV1Route.POST("/users/2fa/enable/confirm.json", bindApiWithTokenUpdate(api.TwoFactorAuthorizations.TwoFactorEnableConfirmHandler, config))
apiV1Route.POST("/users/2fa/disable.json", bindApi(api.TwoFactorAuthorizations.TwoFactorDisableHandler))
apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler))
}
// Data
apiV1Route.GET("/data/statistics.json", bindApi(api.DataManagements.DataStatisticsHandler))
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
if config.EnableDataExport {
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
}
// Accounts
apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler))
apiV1Route.GET("/accounts/get.json", bindApi(api.Accounts.AccountGetHandler))
@@ -274,6 +344,23 @@ func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
}
}
func bindApiWithTokenUpdate(fn core.ApiHandlerFunc, config *settings.Config) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, err := fn(c)
if err == nil && config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
middlewares.AmapApiProxyAuthCookie(c, config)
}
if err != nil {
utils.PrintJsonErrorResult(c, err)
} else {
utils.PrintJsonSuccessResult(c, result)
}
}
}
func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
@@ -286,3 +373,29 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
}
}
}
func bindCachedPngImage(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, _, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, "img/png", "", result)
}
})
}
func bindProxy(fn core.ProxyHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
proxy, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
proxy.ServeHTTP(c.Writer, c.Request)
}
}
}
+100 -13
View File
@@ -28,13 +28,13 @@ cert_key_file =
# Unix socket path, for "socket" only
unix_socket =
# Static file root path (relative or absolute)
# Static file root path (relative or absolute path)
static_root_path = public
# Enable GZip
enable_gzip = false
# Set to true to log each request and execution times
# Set to true to log each request and execution time
log_request = true
[database]
@@ -50,24 +50,37 @@ passwd =
# For "postgres" only, Either "disable", "require" or "verify-full"
ssl_mode = disable
# For "sqlite3" only, absolute path of db file
# For "sqlite3" only, db file path (relative or absolute path)
db_path = data/ezbookkeeping.db
# Max idle connection number, default is 2
# Max idle connection number (0 - 65535, 0 means no idle connections are retained), default is 2
max_idle_conn = 2
# Max opened connection number, default is 0 (unlimited)
# Max opened connection number (0 - 65535), default is 0 (unlimited)
max_open_conn = 0
# Max connection lifetime (seconds), default is 14400 (4 hours)
# Max connection lifetime (0 - 4294967295 seconds), default is 14400 (4 hours)
conn_max_lifetime = 14400
# Set to true to log each sql statement and execution times
# Set to true to log each sql statement and execution time
log_query = false
# Set to true to automatically update database structure when starting web server
auto_update_database = true
[mail]
# Set to true to enable sending mail by SMTP server
enable_smtp = false
# SMTP Server connection configuration
smtp_host = 127.0.0.1:25
smtp_user =
smtp_passwd =
smtp_skip_tls_verify = false
# Mail from address. This can be just an email address, or the "Name" <user@domain.com> format.
from_address =
[log]
# Either "console", "file", default is "console"
# Use space to separate multiple modes, e.g. "console file"
@@ -76,14 +89,14 @@ mode = console file
# Either "debug", "info", "warn", "error", default is "info"
level = info
# For "file" only, absolute path of log file
# For "file" only, log file path (relative or absolute path)
log_path = log/ezbookkeeping.log
[uuid]
# Uuid generator type, supports "internal" currently
generator_type = internal
# For "internal" only, each server must have unique id
# For "internal" only, each server must have unique id (0 - 255)
server_id = 0
[security]
@@ -93,12 +106,18 @@ secret_key =
# Set to true to enable two factor authorization
enable_two_factor = true
# Token expired seconds, default is 2592000 (30 days)
# Token expired seconds (0 - 4294967295), default is 2592000 (30 days)
token_expired_time = 2592000
# Temporary token expired seconds, default is 300 (5 minutes)
# Temporary token expired seconds (0 - 4294967295), default is 300 (5 minutes)
temporary_token_expired_time = 300
# Email verify token expired seconds (0 - 4294967295), default is 3600 (60 minutes)
email_verify_token_expired_time = 3600
# Password reset token expired seconds (0 - 4294967295), default is 3600 (60 minutes)
password_reset_token_expired_time = 3600
# Add X-Request-Id header to response to track user request or error, default is true
request_id_header = true
@@ -106,13 +125,81 @@ request_id_header = true
# Set to true to allow users to register account by themselves
enable_register = true
# Set to true to allow users to verify email address
enable_email_verify = false
# Set to true to require email must be verified when login
enable_force_email_verify = false
# Set to true to allow users to reset password
enable_forget_password = true
# Set to true to require email must be verified when use forget password
forget_password_require_email_verify = false
# User avatar provider, supports the following types:
# "gravatar": https://gravatar.com
# Leave blank if you want to disable user avatar
avatar_provider =
[data]
# Set to true to allow users to export their data
enable_export = true
[map]
# Map provider, supports the following types:
# "openstreetmap": https://www.openstreetmap.org
# "openstreetmap_humanitarian": http://map.hotosm.org
# "opentopomap": https://opentopomap.org
# "opnvkarte": https://publictransportmap.org
# "cyclosm": https://www.cyclosm.org
# "tomtom": https://www.tomtom.com
# "googlemap": https://map.google.com
# "baidumap": https://map.baidu.com
# "amap": https://amap.com
# Leave blank if you want to disable map
map_provider = openstreetmap
# Set to true to use the ezbookkeeping server to proxy map data requests, for "openstreetmap", "openstreetmap_humanitarian", "opentopomap", "opnvkarte", "cyclosm" or "tomtom"
map_data_fetch_proxy = false
# For "tomtom" only, TomTom map API key, please visit https://developer.tomtom.com/how-to-get-tomtom-api-key
tomtom_map_api_key =
# For "googlemap" only, Google map JavaScript API key, please visit https://developers.google.com/maps/get-started for more information
google_map_api_key =
# For "baidumap" only, Baidu map JavaScript API application key, please visit https://lbsyun.baidu.com/index.php?title=jspopular3.0/guide/getkey for more information
baidu_map_ak =
# For "amap" only, Amap JavaScript API application key, please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
amap_application_key =
# For "amap" only, Amap JavaScript API security verification method, supports the following methods:
# "internal_proxy": use the internal proxy to request amap api with amap application secret (default)
# "external_proxy": use an external proxy to request amap api (amap application secret should be set by external proxy)
# "plain_text": append amap application secret to frontend request directly (insecurity for public network)
# Please visit https://developer.amap.com/api/jsapi-v2/guide/abc/load for more information
amap_security_verification_method = plain_text
# For "amap" only, Amap JavaScript API application secret, this setting must be provided when "amap_security_verification_method" is set to "internal_proxy" or "plain_text", please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
amap_application_secret =
# For "amap" only, Amap JavaScript API external proxy url, this setting must be provided when "amap_security_verification_method" is set to "external_proxy"
amap_api_external_proxy_url =
[exchange_rates]
# Exchange rates data source, supports "euro_central_bank", "bank_of_canada", "reserve_bank_of_australia", "czech_national_bank", "national_bank_of_poland" currently
# Exchange rates data source, supports the following types:
# "euro_central_bank"
# "bank_of_canada"
# "reserve_bank_of_australia",
# "czech_national_bank"
# "national_bank_of_poland"
# "monetary_authority_of_singapore"
data_source = euro_central_bank
# Requesting exchange rates data timeout (milliseconds), default is 10000 (10 seconds)
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds), default is 10000 (10 seconds)
request_timeout = 10000
# Set to true skip tls verification when request exchange rates data
skip_tls_verify = false
+10
View File
@@ -9,6 +9,7 @@ import (
"github.com/urfave/cli/v2"
"github.com/mayswind/ezbookkeeping/cmd"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -24,6 +25,9 @@ var (
)
func main() {
settings.Version = Version
settings.CommitHash = CommitHash
app := &cli.App{
Name: "ezBookkeeping",
Usage: "A lightweight personal bookkeeping app hosted by yourself.",
@@ -32,12 +36,18 @@ func main() {
cmd.WebServer,
cmd.Database,
cmd.UserData,
cmd.SecurityUtils,
cmd.Utilities,
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "conf-path",
Usage: "Custom config `FILE` path",
},
&cli.BoolFlag{
Name: "no-boot-log",
Usage: "Disable boot log",
},
},
}
+60 -16
View File
@@ -1,21 +1,65 @@
module github.com/mayswind/ezbookkeeping
go 1.14
go 1.20
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/gzip v0.0.3
github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.4.1
github.com/go-sql-driver/mysql v1.5.0
github.com/lib/pq v1.8.0
github.com/mattn/go-sqlite3 v1.14.4
github.com/pquerna/otp v1.3.0
github.com/sirupsen/logrus v1.7.0
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.6.1
github.com/urfave/cli/v2 v2.3.0
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
gopkg.in/ini.v1 v1.62.0
xorm.io/xorm v1.0.5
github.com/boombuler/barcode v1.0.1
github.com/gin-contrib/cache v1.2.0
github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.15.1
github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.16
github.com/pquerna/otp v1.4.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.25.7
github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/crypto v0.12.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/mail.v2 v2.3.1
xorm.io/xorm v1.3.2
)
require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
xorm.io/builder v0.3.12 // indirect
)
+711 -74
View File
@@ -1,141 +1,778 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cache v1.2.0 h1:WA+AJR4kmHDTaLLShCHo/IeWVmmGRZ3Lsr3JQ46tFlE=
github.com/gin-contrib/cache v1.2.0/go.mod h1:2KkFL8PSnPF3Tt5E2Jpc3HWuBAUKqGZnClCFMm0tXQI=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.0.5 h1:LRr5PfOUb4ODPR63YwbowkNDwcolT2LnkwP/TUaMaB0=
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE=
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=
+8162 -12525
View File
File diff suppressed because it is too large Load Diff
+41 -44
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "0.1.0",
"version": "0.4.0",
"private": true,
"repository": {
"type": "git",
@@ -12,56 +12,53 @@
"url": "https://github.com/mayswind/ezbookkeeping/issues"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"serve": "cross-env NODE_ENV=development vite",
"build": "cross-env NODE_ENV=production vite build",
"serve:dist": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"axios": "^0.21.1",
"@mdi/js": "^7.2.96",
"@vuepic/vue-datepicker": "^5.4.0",
"axios": "^1.4.0",
"cbor-js": "^0.1.0",
"core-js": "^3.6.5",
"crypto-js": "^4.0.0",
"framework7": "^5.7.14",
"framework7-icons": "^3.0.1",
"framework7-vue": "^5.7.14",
"js-cookie": "^2.2.1",
"clipboard": "^2.0.11",
"crypto-js": "^4.1.1",
"dom7": "^4.0.6",
"echarts": "^5.4.3",
"framework7": "^8.3.0",
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.3.0",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"line-awesome": "^1.3.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"pinia": "^2.1.6",
"register-service-worker": "^1.7.2",
"ua-parser-js": "^0.7.28",
"vue": "^2.6.12",
"vue-clipboard2": "^0.3.1",
"vue-i18n": "^8.24.3",
"vue-pincode-input": "^0.4.0",
"vuex": "^3.6.2"
"skeleton-elements": "^4.0.1",
"swiper": "^10.2.0",
"ua-parser-js": "^1.0.35",
"vue": "^3.3.4",
"vue-echarts": "^6.6.1",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.4",
"vue3-perfect-scrollbar": "^1.6.1",
"vuedraggable": "^4.1.0",
"vuetify": "^3.3.16"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.11",
"@vue/cli-plugin-eslint": "^4.5.11",
"@vue/cli-plugin-pwa": "^4.5.11",
"@vue/cli-service": "^4.5.11",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"git-revision-webpack-plugin": "^3.0.6",
"moment-locales-webpack-plugin": "^1.2.0",
"vue-template-compiler": "^2.6.12"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
"@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.4",
"cross-env": "^7.0.3",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"git-rev-sync": "^3.0.2",
"postcss-preset-env": "^9.1.1",
"sass": "^1.66.1",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-vuetify": "^1.0.2"
},
"browserslist": [
"> 1%",
+29 -22
View File
@@ -24,7 +24,7 @@ var (
)
// AccountListHandler returns accounts list of current user
func (a *AccountsApi) AccountListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountListHandler(c *core.Context) (any, *errs.Error) {
var accountListReq models.AccountListRequest
err := c.ShouldBindQuery(&accountListReq)
@@ -34,11 +34,11 @@ func (a *AccountsApi) AccountListHandler(c *core.Context) (interface{}, *errs.Er
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(uid)
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountListHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
userAllAccountResps := make([]*models.AccountInfoResponse, len(accounts))
@@ -84,7 +84,7 @@ func (a *AccountsApi) AccountListHandler(c *core.Context) (interface{}, *errs.Er
}
// AccountGetHandler returns one specific account of current user
func (a *AccountsApi) AccountGetHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountGetHandler(c *core.Context) (any, *errs.Error) {
var accountGetReq models.AccountGetRequest
err := c.ShouldBindQuery(&accountGetReq)
@@ -94,11 +94,11 @@ func (a *AccountsApi) AccountGetHandler(c *core.Context) (interface{}, *errs.Err
}
uid := c.GetCurrentUid()
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(uid, accountGetReq.Id)
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountGetHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountGetReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
accountRespMap := make(map[int64]*models.AccountInfoResponse)
@@ -127,7 +127,7 @@ func (a *AccountsApi) AccountGetHandler(c *core.Context) (interface{}, *errs.Err
}
// AccountCreateHandler saves a new account by request parameters for current user
func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
var accountCreateReq models.AccountCreateRequest
err := c.ShouldBindJSON(&accountCreateReq)
@@ -136,6 +136,13 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
if accountCreateReq.Type == models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
if len(accountCreateReq.SubAccounts) > 0 {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot have any sub accounts")
@@ -186,17 +193,17 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
}
uid := c.GetCurrentUid()
maxOrderId, err := a.accounts.GetMaxDisplayOrder(uid, accountCreateReq.Category)
maxOrderId, err := a.accounts.GetMaxDisplayOrder(c, uid, accountCreateReq.Category)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
err = a.accounts.CreateAccounts(mainAccount, childrenAccounts)
err = a.accounts.CreateAccounts(c, mainAccount, childrenAccounts, utcOffset)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to create account \"id:%d\" for user \"uid:%d\", because %s", mainAccount.AccountId, uid, err.Error())
@@ -219,7 +226,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
}
// AccountModifyHandler saves an existed account by request parameters for current user
func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
var accountModifyReq models.AccountModifyRequest
err := c.ShouldBindJSON(&accountModifyReq)
@@ -229,11 +236,11 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.
}
uid := c.GetCurrentUid()
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(uid, accountModifyReq.Id)
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountModifyHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
@@ -275,7 +282,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.
return nil, errs.ErrNothingWillBeUpdated
}
err = a.accounts.ModifyAccounts(uid, toUpdateAccounts)
err = a.accounts.ModifyAccounts(c, uid, toUpdateAccounts)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountModifyHandler] failed to update account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
@@ -325,7 +332,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.
}
// AccountHideHandler hides an existed account by request parameters for current user
func (a *AccountsApi) AccountHideHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountHideHandler(c *core.Context) (any, *errs.Error) {
var accountHideReq models.AccountHideRequest
err := c.ShouldBindJSON(&accountHideReq)
@@ -335,7 +342,7 @@ func (a *AccountsApi) AccountHideHandler(c *core.Context) (interface{}, *errs.Er
}
uid := c.GetCurrentUid()
err = a.accounts.HideAccount(uid, []int64{accountHideReq.Id}, accountHideReq.Hidden)
err = a.accounts.HideAccount(c, uid, []int64{accountHideReq.Id}, accountHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountHideHandler] failed to hide account \"id:%d\" for user \"uid:%d\", because %s", accountHideReq.Id, uid, err.Error())
@@ -347,7 +354,7 @@ func (a *AccountsApi) AccountHideHandler(c *core.Context) (interface{}, *errs.Er
}
// AccountMoveHandler moves display order of existed accounts by request parameters for current user
func (a *AccountsApi) AccountMoveHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountMoveHandler(c *core.Context) (any, *errs.Error) {
var accountMoveReq models.AccountMoveRequest
err := c.ShouldBindJSON(&accountMoveReq)
@@ -370,7 +377,7 @@ func (a *AccountsApi) AccountMoveHandler(c *core.Context) (interface{}, *errs.Er
accounts[i] = account
}
err = a.accounts.ModifyAccountDisplayOrders(uid, accounts)
err = a.accounts.ModifyAccountDisplayOrders(c, uid, accounts)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountMoveHandler] failed to move accounts for user \"uid:%d\", because %s", uid, err.Error())
@@ -382,7 +389,7 @@ func (a *AccountsApi) AccountMoveHandler(c *core.Context) (interface{}, *errs.Er
}
// AccountDeleteHandler deletes an existed account by request parameters for current user
func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (any, *errs.Error) {
var accountDeleteReq models.AccountDeleteRequest
err := c.ShouldBindJSON(&accountDeleteReq)
@@ -392,7 +399,7 @@ func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (interface{}, *errs.
}
uid := c.GetCurrentUid()
err = a.accounts.DeleteAccount(uid, accountDeleteReq.Id)
err = a.accounts.DeleteAccount(c, uid, accountDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountDeleteHandler] failed to delete account \"id:%d\" for user \"uid:%d\", because %s", accountDeleteReq.Id, uid, err.Error())
@@ -403,7 +410,7 @@ func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (interface{}, *errs.
return true, nil
}
func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.AccountCreateRequest, order int) *models.Account {
func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.AccountCreateRequest, order int32) *models.Account {
return &models.Account{
Uid: uid,
Name: accountCreateReq.Name,
@@ -425,7 +432,7 @@ func (a *AccountsApi) createSubAccountModels(uid int64, accountCreateReq *models
childrenAccounts := make([]*models.Account, len(accountCreateReq.SubAccounts))
for i := 0; i < len(accountCreateReq.SubAccounts); i++ {
for i := int32(0); i < int32(len(accountCreateReq.SubAccounts)); i++ {
childrenAccounts[i] = a.createNewAccountModel(uid, accountCreateReq.SubAccounts[i], i+1)
}
+61
View File
@@ -0,0 +1,61 @@
package api
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const amapCustomMapStylesUrl = "https://webapi.amap.com/v4/map/styles"
const amapOverseasMapUrl = "https://fmap01.amap.com/v3/vectormap"
const amapRestApiUrl = "https://restapi.amap.com/"
// AmapApiProxy represents amap api proxy
type AmapApiProxy struct {
}
// Initialize a amap api proxy singleton instance
var (
AmapApis = &AmapApiProxy{}
)
// AmapApiProxyHandler returns amap api response
func (p *AmapApiProxy) AmapApiProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
var targetUrl string
if strings.HasPrefix(c.Request.RequestURI, "/_AMapService/v4/map/styles") {
targetUrl = amapCustomMapStylesUrl + strings.TrimPrefix(c.Request.URL.Path, "/_AMapService/v4/map/styles")
} else if strings.HasPrefix(c.Request.RequestURI, "/_AMapService/v3/vectormap") {
targetUrl = amapOverseasMapUrl + strings.TrimPrefix(c.Request.URL.Path, "/_AMapService/v3/vectormap")
} else {
targetUrl = amapRestApiUrl + strings.TrimPrefix(c.Request.URL.Path, "/_AMapService/")
}
director := func(req *http.Request) {
targetRawUrl := fmt.Sprintf("%s?%s&jscode=%s", targetUrl, req.URL.RawQuery, settings.Container.Current.AmapApplicationSecret)
targetUrl, _ := url.Parse(targetRawUrl)
oldCookies := req.Cookies()
req.Header.Del("Cookie")
for i := 0; i < len(oldCookies); i++ {
if strings.HasPrefix(oldCookies[i].Name, "ebk_") {
continue
}
req.AddCookie(oldCookies[i])
}
req.URL = targetUrl
req.RequestURI = req.URL.RequestURI()
req.Host = targetUrl.Host
}
return &httputil.ReverseProxy{Director: director}, nil
}
+65 -17
View File
@@ -8,6 +8,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// AuthorizationsApi represents authorization api
@@ -27,7 +28,7 @@ var (
)
// AuthorizeHandler verifies and authorizes current login request
func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error) {
var credential models.UserLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -36,14 +37,35 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
return nil, errs.ErrLoginNameOrPasswordInvalid
}
user, err := a.users.GetUserByUsernameOrEmailAndPassword(credential.LoginName, credential.Password)
user, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
return nil, errs.ErrLoginNameOrPasswordWrong
}
err = a.users.UpdateUserLastLoginTime(user.Uid)
if user.Disabled {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user is disabled", credential.LoginName)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
hasValidEmailVerifyToken, err := a.tokens.ExistsValidTokenByType(c, user.Uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed check whether user \"uid:%d\" has valid verify email token, because %s", user.Uid, err.Error())
hasValidEmailVerifyToken = false
}
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user has not verified email", credential.LoginName)
return nil, errs.NewErrorWithContext(errs.ErrEmailIsNotVerified, map[string]any{
"email": user.Email,
"hasValidEmailVerifyToken": hasValidEmailVerifyToken,
})
}
err = a.users.UpdateUserLastLoginTime(c, user.Uid)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -52,7 +74,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
twoFactorEnable := a.tokens.CurrentConfig().EnableTwoFactor
if twoFactorEnable {
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(user.Uid)
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, user.Uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -64,9 +86,9 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
var claims *core.UserTokenClaims
if twoFactorEnable {
token, claims, err = a.tokens.CreateRequire2FAToken(user, c)
token, claims, err = a.tokens.CreateRequire2FAToken(c, user)
} else {
token, claims, err = a.tokens.CreateToken(user, c)
token, claims, err = a.tokens.CreateToken(c, user)
}
if err != nil {
@@ -74,6 +96,10 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
return nil, errs.ErrTokenGenerating
}
if !twoFactorEnable {
c.SetTextualToken(token)
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
@@ -83,7 +109,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
}
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *errs.Error) {
var credential models.TwoFactorLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -93,7 +119,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac
}
uid := c.GetCurrentUid()
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
@@ -105,27 +131,38 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac
return nil, errs.ErrPasscodeInvalid
}
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
@@ -135,7 +172,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac
}
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (any, *errs.Error) {
var credential models.TwoFactorRecoveryCodeLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -145,7 +182,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
}
uid := c.GetCurrentUid()
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
@@ -156,14 +193,24 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
return nil, errs.ErrTwoFactorIsNotEnabled
}
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
return nil, errs.ErrUserNotFound
}
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(uid, credential.RecoveryCode, user.Salt)
if user.Disabled {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
@@ -171,19 +218,20 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
}
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has authorized two factor via recovery code \"%s\", token will be expired at %d", user.Uid, credential.RecoveryCode, claims.ExpiresAt)
+55 -14
View File
@@ -57,7 +57,7 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -67,28 +67,28 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
return nil, "", errs.ErrUserNotFound
}
accounts, err := a.accounts.GetAllAccountsByUid(uid)
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
categories, err := a.categories.GetAllCategoriesByUid(uid, 0, -1)
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tags, err := a.tags.GetAllTagsByUid(uid)
tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(uid)
tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
@@ -99,7 +99,7 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
categoryMap := a.categories.GetCategoryMapByList(categories)
tagMap := a.tags.GetTagMapByList(tags)
allTransactions, err := a.transactions.GetAllTransactions(uid, pageCountForDataExport, true)
allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
@@ -118,8 +118,49 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
return result, fileName, nil
}
// DataStatisticsHandler returns user data statistics
func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
totalAccountCount, err := a.accounts.GetTotalAccountCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total account count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalTransactionCategoryCount, err := a.categories.GetTotalCategoryCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction category count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalTransactionTagCount, err := a.tags.GetTotalTagCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction tag count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalTransactionCount, err := a.transactions.GetTotalTransactionCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
dataStatisticsResp := &models.DataStatisticsResponse{
TotalAccountCount: totalAccountCount,
TotalTransactionCategoryCount: totalTransactionCategoryCount,
TotalTransactionTagCount: totalTransactionTagCount,
TotalTransactionCount: totalTransactionCount,
}
return dataStatisticsResp, nil
}
// ClearDataHandler deletes all user data
func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error) {
var clearDataReq models.ClearDataRequest
err := c.ShouldBindJSON(&clearDataReq)
@@ -129,7 +170,7 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (interface{}, *er
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -143,25 +184,25 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (interface{}, *er
return nil, errs.ErrUserPasswordWrong
}
err = a.transactions.DeleteAllTransactions(uid)
err = a.transactions.DeleteAllTransactions(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.categories.DeleteAllCategories(uid)
err = a.categories.DeleteAllCategories(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.tags.DeleteAllTags(uid)
err = a.tags.DeleteAllTags(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
+2 -2
View File
@@ -14,11 +14,11 @@ var (
)
// ApiNotFound returns api not found error
func (a *DefaultApi) ApiNotFound(c *core.Context) (interface{}, *errs.Error) {
func (a *DefaultApi) ApiNotFound(c *core.Context) (any, *errs.Error) {
return nil, errs.ErrApiNotFound
}
// MethodNotAllowed returns method not allowed error
func (a *DefaultApi) MethodNotAllowed(c *core.Context) (interface{}, *errs.Error) {
func (a *DefaultApi) MethodNotAllowed(c *core.Context) (any, *errs.Error) {
return nil, errs.ErrMethodNotAllowed
}
+14 -4
View File
@@ -1,7 +1,8 @@
package api
import (
"io/ioutil"
"crypto/tls"
"io"
"net/http"
"sort"
"time"
@@ -23,7 +24,7 @@ var (
)
// LatestExchangeRateHandler returns latest exchange rate data
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *errs.Error) {
dataSource := exchangerates.Container.Current
if dataSource == nil {
@@ -32,8 +33,17 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (interface
uid := c.GetCurrentUid()
transport := http.DefaultTransport.(*http.Transport).Clone()
if settings.Container.Current.ExchangeRatesSkipTLSVerify {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
client := &http.Client{
Timeout: time.Duration(settings.Container.Current.ExchangeRatesRequestTimeout) * time.Millisecond,
Transport: transport,
Timeout: time.Duration(settings.Container.Current.ExchangeRatesRequestTimeout) * time.Millisecond,
}
urls := dataSource.GetRequestUrls()
@@ -53,7 +63,7 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (interface
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
exchangeRateResp, err := dataSource.Parse(c, body)
if err != nil {
+152
View File
@@ -0,0 +1,152 @@
package api
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// ForgetPasswordsApi represents user forget password api
type ForgetPasswordsApi struct {
users *services.UserService
tokens *services.TokenService
forgetPasswords *services.ForgetPasswordService
}
// Initialize a user api singleton instance
var (
ForgetPasswords = &ForgetPasswordsApi{
users: services.Users,
tokens: services.Tokens,
forgetPasswords: services.ForgetPasswords,
}
)
// UserForgetPasswordRequestHandler generates password reset link and send user an email with this link
func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (any, *errs.Error) {
var request models.ForgetPasswordRequest
err := c.ShouldBindJSON(&request)
if err != nil {
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] parse request failed, because %s", err.Error())
return nil, errs.ErrEmailIsEmptyOrInvalid
}
user, err := a.users.GetUserByEmail(c, request.Email)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
if !settings.Container.Current.EnableSMTP {
return nil, errs.ErrSMTPServerNotEnabled
}
token, _, err := a.tokens.CreatePasswordResetToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
go func() {
err = a.forgetPasswords.SendPasswordResetEmail(c, user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
}
}()
return true, nil
}
// UserResetPasswordHandler resets user password by request parameters
func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *errs.Error) {
var request models.PasswordResetRequest
err := c.ShouldBindJSON(&request)
if err != nil {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
if user.Email != request.Email {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
return nil, errs.ErrEmptyIsInvalid
}
if a.users.IsPasswordEqualsUserPassword(request.Password, user) {
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke password reset token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
return nil, errs.ErrNewPasswordEqualsOldInvalid
}
userNew := &models.User{
Uid: user.Uid,
Salt: user.Salt,
Password: request.Password,
}
_, _, err = a.users.UpdateUser(c, userNew, false)
if err != nil {
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
if err == nil {
log.InfofWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
} else {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
return true, nil
}
+26
View File
@@ -0,0 +1,26 @@
package api
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// HealthsApi represents health api
type HealthsApi struct{}
// Initialize a healths api singleton instance
var (
Healths = &HealthsApi{}
)
// HealthStatusHandler returns the health status of current service
func (a *HealthsApi) HealthStatusHandler(c *core.Context) (any, *errs.Error) {
result := make(map[string]string)
result["version"] = settings.Version
result["commit"] = settings.CommitHash
result["status"] = "ok"
return result, nil
}
+71
View File
@@ -0,0 +1,71 @@
package api
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/%s/%s/%s" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
const openStreetMapHumanitarianStyleTileImageUrlFormat = "https://a.tile.openstreetmap.fr/hot/%s/%s/%s" // https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png
const openTopoMapTileImageUrlFormat = "https://tile.opentopomap.org/%s/%s/%s" // https://tile.opentopomap.org/{z}/{x}/{y}.png
const opnvKarteMapTileImageUrlFormat = "https://tileserver.memomaps.de/tilegen/%s/%s/%s" // https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png
const cyclOSMMapTileImageUrlFormat = "https://a.tile-cyclosm.openstreetmap.fr/cyclosm/%s/%s/%s" // https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png
const tomtomMapTileImageUrlFormat = "https://api.tomtom.com/map/1/tile/basic/main/%s/%s/%s" // https://api.tomtom.com/map/{versionNumber}/tile/{layer}/{style}/{z}/{x}/{y}.png?key={key}&language={language}
// MapImageProxy represents map image proxy
type MapImageProxy struct {
}
// Initialize a map image proxy singleton instance
var (
MapImages = &MapImageProxy{}
)
// MapTileImageProxyHandler returns map tile image
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
mapProvider := strings.Replace(c.Query("provider"), "-", "_", -1)
targetUrl := ""
if mapProvider == settings.OpenStreetMapProvider {
targetUrl = openStreetMapTileImageUrlFormat
} else if mapProvider == settings.OpenStreetMapHumanitarianStyleProvider {
targetUrl = openStreetMapHumanitarianStyleTileImageUrlFormat
} else if mapProvider == settings.OpenTopoMapProvider {
targetUrl = openTopoMapTileImageUrlFormat
} else if mapProvider == settings.OPNVKarteMapProvider {
targetUrl = opnvKarteMapTileImageUrlFormat
} else if mapProvider == settings.CyclOSMMapProvider {
targetUrl = cyclOSMMapTileImageUrlFormat
} else if mapProvider == settings.TomTomMapProvider {
targetUrl = tomtomMapTileImageUrlFormat + "?key=" + settings.Container.Current.TomTomMapAPIKey
language := c.Query("language")
if language != "" {
targetUrl = targetUrl + "&language=" + language
}
} else {
return nil, errs.ErrParameterInvalid
}
director := func(req *http.Request) {
zoomLevel := c.Param("zoomLevel")
coordinateX := c.Param("coordinateX")
fileName := c.Param("fileName")
imageRawUrl := fmt.Sprintf(targetUrl, zoomLevel, coordinateX, fileName)
imageUrl, _ := url.Parse(imageRawUrl)
req.URL = imageUrl
req.RequestURI = req.URL.RequestURI()
req.Host = imageUrl.Host
}
return &httputil.ReverseProxy{Director: director}, nil
}
+51
View File
@@ -0,0 +1,51 @@
package api
import (
"bytes"
"image/png"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/qr"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const (
qrCodeDefaultWidth int = 320
qrCodeDefaultHeight int = 320
)
// QrCodesApi represents qrcode generator api
type QrCodesApi struct {
}
// Initialize a qrcode generator api singleton instance
var (
QrCodes = &QrCodesApi{}
)
// MobileUrlQrCodeHandler returns a mobile url qr code image
func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.Context) ([]byte, string, *errs.Error) {
fullUrl := settings.Container.Current.RootUrl + "mobile"
data, err := a.generateUrlQrCode(c, fullUrl)
if err != nil {
return nil, "", errs.ErrOperationFailed
}
return data, "", nil
}
func (a *QrCodesApi) generateUrlQrCode(c *core.Context, url string) ([]byte, *errs.Error) {
qrCodeImg, _ := qr.Encode(url, qr.M, qr.Auto)
qrCodeImg, _ = barcode.Scale(qrCodeImg, qrCodeDefaultWidth, qrCodeDefaultHeight)
imgData := &bytes.Buffer{}
if err := png.Encode(imgData, qrCodeImg); err != nil {
return nil, errs.ErrOperationFailed
}
return imgData.Bytes(), nil
}
+22 -28
View File
@@ -26,13 +26,13 @@ var (
)
// TokenListHandler returns available token list of current user
func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TokensApi) TokenListHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(uid)
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tokenResps := make(models.TokenInfoResponseSlice, len(tokens))
@@ -48,7 +48,7 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error)
ExpiredAt: token.ExpiredUnixTime,
}
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
tokenResp.IsCurrent = true
}
@@ -61,18 +61,11 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error)
}
// TokenRevokeCurrentHandler revokes current token of current user
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *errs.Error) {
_, claims, err := a.tokens.ParseToken(c)
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (any, *errs.Error) {
_, claims, err := a.tokens.ParseTokenByHeader(c)
if err != nil {
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid, err := utils.StringToInt64(claims.Id)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.TokenRevokeCurrentHandler] parse user id failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
@@ -83,25 +76,25 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *er
}
tokenRecord := &models.TokenRecord{
Uid: uid,
Uid: claims.Uid,
UserTokenId: userTokenId,
CreatedUnixTime: claims.IssuedAt,
}
tokenId := a.tokens.GenerateTokenId(tokenRecord)
err = a.tokens.DeleteToken(tokenRecord)
err = a.tokens.DeleteToken(c, tokenRecord)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
log.ErrorfWithRequestId(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenId)
log.InfofWithRequestId(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
return true, nil
}
// TokenRevokeHandler revokes specific token of current user
func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TokensApi) TokenRevokeHandler(c *core.Context) (any, *errs.Error) {
var tokenRevokeReq models.TokenRevokeRequest
err := c.ShouldBindJSON(&tokenRevokeReq)
@@ -127,7 +120,7 @@ func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Erro
return nil, errs.ErrInvalidTokenId
}
err = a.tokens.DeleteToken(tokenRecord)
err = a.tokens.DeleteToken(c, tokenRecord)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
@@ -139,13 +132,13 @@ func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Erro
}
// TokenRevokeAllHandler revokes all tokens of current user except current token
func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
tokens, err := a.tokens.GetAllTokensByUid(uid)
tokens, err := a.tokens.GetAllTokensByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
claims := c.GetTokenClaims()
@@ -154,7 +147,7 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
for i := 0; i < len(tokens); i++ {
token := tokens[i]
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
currentTokenIndex = i
break
}
@@ -162,7 +155,7 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
tokens = append(tokens[:currentTokenIndex], tokens[currentTokenIndex+1:]...)
err = a.tokens.DeleteTokens(uid, tokens)
err = a.tokens.DeleteTokens(c, uid, tokens)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
@@ -174,16 +167,16 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
}
// TokenRefreshHandler refresh current token of current user
func (a *TokensApi) TokenRefreshHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
return nil, errs.ErrUserNotFound
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -198,6 +191,7 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (interface{}, *errs.Err
CreatedUnixTime: oldTokenClaims.IssuedAt,
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
+81 -68
View File
@@ -3,6 +3,8 @@ package api
import (
"sort"
"github.com/gin-gonic/gin/binding"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
@@ -23,7 +25,7 @@ var (
)
// CategoryListHandler returns transaction category list of current user
func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (any, *errs.Error) {
var categoryListReq models.TransactionCategoryListRequest
err := c.ShouldBindQuery(&categoryListReq)
@@ -33,18 +35,18 @@ func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (interfa
}
uid := c.GetCurrentUid()
categories, err := a.categories.GetAllCategoriesByUid(uid, categoryListReq.Type, categoryListReq.ParentId)
categories, err := a.categories.GetAllCategoriesByUid(c, uid, categoryListReq.Type, categoryListReq.ParentId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryListHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return a.getTransactionCategoryListByTypeResponse(categories, categoryListReq.ParentId)
}
// CategoryGetHandler returns one specific transaction category of current user
func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (any, *errs.Error) {
var categoryGetReq models.TransactionCategoryGetRequest
err := c.ShouldBindQuery(&categoryGetReq)
@@ -54,11 +56,11 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (interfac
}
uid := c.GetCurrentUid()
category, err := a.categories.GetCategoryByCategoryId(uid, categoryGetReq.Id)
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryGetHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryGetReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
categoryResp := category.ToTransactionCategoryInfoResponse()
@@ -67,7 +69,7 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (interfac
}
// CategoryCreateHandler saves a new transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any, *errs.Error) {
var categoryCreateReq models.TransactionCategoryCreateRequest
err := c.ShouldBindJSON(&categoryCreateReq)
@@ -84,7 +86,7 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
uid := c.GetCurrentUid()
if categoryCreateReq.ParentId > 0 {
parentCategory, err := a.categories.GetCategoryByCategoryId(uid, categoryCreateReq.ParentId)
parentCategory, err := a.categories.GetCategoryByCategoryId(c, uid, categoryCreateReq.ParentId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get parent category \"id:%d\" for user \"uid:%d\", because %s", categoryCreateReq.ParentId, uid, err.Error())
@@ -102,22 +104,22 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
}
}
var maxOrderId int
var maxOrderId int32
if categoryCreateReq.ParentId <= 0 {
maxOrderId, err = a.categories.GetMaxDisplayOrder(uid, categoryCreateReq.Type)
maxOrderId, err = a.categories.GetMaxDisplayOrder(c, uid, categoryCreateReq.Type)
} else {
maxOrderId, err = a.categories.GetMaxSubCategoryDisplayOrder(uid, categoryCreateReq.Type, categoryCreateReq.ParentId)
maxOrderId, err = a.categories.GetMaxSubCategoryDisplayOrder(c, uid, categoryCreateReq.Type, categoryCreateReq.ParentId)
}
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
err = a.categories.CreateCategory(category)
err = a.categories.CreateCategory(c, category)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to create category \"id:%d\" for user \"uid:%d\", because %s", category.CategoryId, uid, err.Error())
@@ -132,9 +134,9 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
}
// CategoryCreateBatchHandler saves some new transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (any, *errs.Error) {
var categoryCreateBatchReq models.TransactionCategoryCreateBatchRequest
err := c.ShouldBindJSON(&categoryCreateBatchReq)
err := c.ShouldBindBodyWith(&categoryCreateBatchReq, binding.JSON)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] parse request failed, because %s", err.Error())
@@ -143,58 +145,17 @@ func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (
uid := c.GetCurrentUid()
categoryTypeMaxOrderMap := make(map[models.TransactionCategoryType]int)
categoriesMap := make(map[*models.TransactionCategory][]*models.TransactionCategory)
categoriesMap[nil] = make([]*models.TransactionCategory, len(categoryCreateBatchReq.Categories))
totalCount := 0
for i := 0; i < len(categoryCreateBatchReq.Categories); i++ {
categoryCreateReq := categoryCreateBatchReq.Categories[i]
var maxOrderId, exists = categoryTypeMaxOrderMap[categoryCreateReq.Type]
if !exists {
maxOrderId, err = a.categories.GetMaxDisplayOrder(uid, categoryCreateReq.Type)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
}
category := a.createNewCategoryModel(uid, &models.TransactionCategoryCreateRequest{
Name: categoryCreateReq.Name,
Type: categoryCreateReq.Type,
Icon: categoryCreateReq.Icon,
Color: categoryCreateReq.Color,
}, maxOrderId+1)
categoriesMap[category] = make([]*models.TransactionCategory, len(categoryCreateReq.SubCategories))
for j := 0; j < len(categoryCreateReq.SubCategories); j++ {
subCategory := a.createNewCategoryModel(uid, categoryCreateReq.SubCategories[j], j+1)
categoriesMap[category][j] = subCategory
totalCount++
}
categoriesMap[nil][i] = category
categoryTypeMaxOrderMap[categoryCreateReq.Type] = maxOrderId + 1
totalCount++
}
categories, err := a.categories.CreateCategories(uid, categoriesMap)
categories, err := a.createBatchCategories(c, uid, &categoryCreateBatchReq)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] user \"uid:%d\" has created categoroies successfully", uid)
return a.getTransactionCategoryListByTypeResponse(categories, 0)
}
// CategoryModifyHandler saves an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any, *errs.Error) {
var categoryModifyReq models.TransactionCategoryModifyRequest
err := c.ShouldBindJSON(&categoryModifyReq)
@@ -204,11 +165,11 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
}
uid := c.GetCurrentUid()
category, err := a.categories.GetCategoryByCategoryId(uid, categoryModifyReq.Id)
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newCategory := &models.TransactionCategory{
@@ -229,7 +190,7 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
return nil, errs.ErrNothingWillBeUpdated
}
err = a.categories.ModifyCategory(newCategory)
err = a.categories.ModifyCategory(c, newCategory)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to update category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
@@ -247,7 +208,7 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
}
// CategoryHideHandler hides an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (any, *errs.Error) {
var categoryHideReq models.TransactionCategoryHideRequest
err := c.ShouldBindJSON(&categoryHideReq)
@@ -257,7 +218,7 @@ func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interfa
}
uid := c.GetCurrentUid()
err = a.categories.HideCategory(uid, []int64{categoryHideReq.Id}, categoryHideReq.Hidden)
err = a.categories.HideCategory(c, uid, []int64{categoryHideReq.Id}, categoryHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryHideHandler] failed to hide category \"id:%d\" for user \"uid:%d\", because %s", categoryHideReq.Id, uid, err.Error())
@@ -269,7 +230,7 @@ func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interfa
}
// CategoryMoveHandler moves display order of existed transaction categories by request parameters for current user
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (any, *errs.Error) {
var categoryMoveReq models.TransactionCategoryMoveRequest
err := c.ShouldBindJSON(&categoryMoveReq)
@@ -292,7 +253,7 @@ func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interfa
categories[i] = category
}
err = a.categories.ModifyCategoryDisplayOrders(uid, categories)
err = a.categories.ModifyCategoryDisplayOrders(c, uid, categories)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error())
@@ -304,7 +265,7 @@ func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interfa
}
// CategoryDeleteHandler deletes an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (any, *errs.Error) {
var categoryDeleteReq models.TransactionCategoryDeleteRequest
err := c.ShouldBindJSON(&categoryDeleteReq)
@@ -314,7 +275,7 @@ func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (inter
}
uid := c.GetCurrentUid()
err = a.categories.DeleteCategory(uid, categoryDeleteReq.Id)
err = a.categories.DeleteCategory(c, uid, categoryDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] failed to delete category \"id:%d\" for user \"uid:%d\", because %s", categoryDeleteReq.Id, uid, err.Error())
@@ -325,7 +286,59 @@ func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (inter
return true, nil
}
func (a *TransactionCategoriesApi) createNewCategoryModel(uid int64, categoryCreateReq *models.TransactionCategoryCreateRequest, order int) *models.TransactionCategory {
func (a *TransactionCategoriesApi) createBatchCategories(c *core.Context, uid int64, categoryCreateBatchReq *models.TransactionCategoryCreateBatchRequest) ([]*models.TransactionCategory, error) {
var err error
categoryTypeMaxOrderMap := make(map[models.TransactionCategoryType]int32)
categoriesMap := make(map[*models.TransactionCategory][]*models.TransactionCategory)
categoriesMap[nil] = make([]*models.TransactionCategory, len(categoryCreateBatchReq.Categories))
totalCount := 0
for i := 0; i < len(categoryCreateBatchReq.Categories); i++ {
categoryCreateReq := categoryCreateBatchReq.Categories[i]
var maxOrderId, exists = categoryTypeMaxOrderMap[categoryCreateReq.Type]
if !exists {
maxOrderId, err = a.categories.GetMaxDisplayOrder(c, uid, categoryCreateReq.Type)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
category := a.createNewCategoryModel(uid, &models.TransactionCategoryCreateRequest{
Name: categoryCreateReq.Name,
Type: categoryCreateReq.Type,
Icon: categoryCreateReq.Icon,
Color: categoryCreateReq.Color,
}, maxOrderId+1)
categoriesMap[category] = make([]*models.TransactionCategory, len(categoryCreateReq.SubCategories))
for j := int32(0); j < int32(len(categoryCreateReq.SubCategories)); j++ {
subCategory := a.createNewCategoryModel(uid, categoryCreateReq.SubCategories[j], j+1)
categoriesMap[category][j] = subCategory
totalCount++
}
categoriesMap[nil][i] = category
categoryTypeMaxOrderMap[categoryCreateReq.Type] = maxOrderId + 1
totalCount++
}
categories, err := a.categories.CreateCategories(c, uid, categoriesMap)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.createBatchCategories] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.createBatchCategories] user \"uid:%d\" has created categories successfully", uid)
return categories, nil
}
func (a *TransactionCategoriesApi) createNewCategoryModel(uid int64, categoryCreateReq *models.TransactionCategoryCreateRequest, order int32) *models.TransactionCategory {
return &models.TransactionCategory{
Uid: uid,
Name: categoryCreateReq.Name,
+21 -21
View File
@@ -23,13 +23,13 @@ var (
)
// TagListHandler returns transaction tag list of current user
func (a *TransactionTagsApi) TagListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagListHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
tags, err := a.tags.GetAllTagsByUid(uid)
tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagListHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tagResps := make(models.TransactionTagInfoResponseSlice, len(tags))
@@ -44,7 +44,7 @@ func (a *TransactionTagsApi) TagListHandler(c *core.Context) (interface{}, *errs
}
// TagGetHandler returns one specific transaction tag of current user
func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (any, *errs.Error) {
var tagGetReq models.TransactionTagGetRequest
err := c.ShouldBindQuery(&tagGetReq)
@@ -54,11 +54,11 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (interface{}, *errs.
}
uid := c.GetCurrentUid()
tag, err := a.tags.GetTagByTagId(uid, tagGetReq.Id)
tag, err := a.tags.GetTagByTagId(c, uid, tagGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagGetHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagGetReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tagResp := tag.ToTransactionTagInfoResponse()
@@ -67,7 +67,7 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (interface{}, *errs.
}
// TagCreateHandler saves a new transaction tag by request parameters for current user
func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (any, *errs.Error) {
var tagCreateReq models.TransactionTagCreateRequest
err := c.ShouldBindJSON(&tagCreateReq)
@@ -78,16 +78,16 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (interface{}, *er
uid := c.GetCurrentUid()
maxOrderId, err := a.tags.GetMaxDisplayOrder(uid)
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tag := a.createNewTagModel(uid, &tagCreateReq, maxOrderId+1)
err = a.tags.CreateTag(tag)
err = a.tags.CreateTag(c, tag)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to create tag \"id:%d\" for user \"uid:%d\", because %s", tag.TagId, uid, err.Error())
@@ -102,7 +102,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (interface{}, *er
}
// TagModifyHandler saves an existed transaction tag by request parameters for current user
func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (any, *errs.Error) {
var tagModifyReq models.TransactionTagModifyRequest
err := c.ShouldBindJSON(&tagModifyReq)
@@ -112,11 +112,11 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *er
}
uid := c.GetCurrentUid()
tag, err := a.tags.GetTagByTagId(uid, tagModifyReq.Id)
tag, err := a.tags.GetTagByTagId(c, uid, tagModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagModifyHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newTag := &models.TransactionTag{
@@ -129,7 +129,7 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *er
return nil, errs.ErrNothingWillBeUpdated
}
err = a.tags.ModifyTag(newTag)
err = a.tags.ModifyTag(c, newTag)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagModifyHandler] failed to update tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
@@ -145,7 +145,7 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *er
}
// TagHideHandler hides an transaction tag by request parameters for current user
func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error) {
var tagHideReq models.TransactionTagHideRequest
err := c.ShouldBindJSON(&tagHideReq)
@@ -155,7 +155,7 @@ func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (interface{}, *errs
}
uid := c.GetCurrentUid()
err = a.tags.HideTag(uid, []int64{tagHideReq.Id}, tagHideReq.Hidden)
err = a.tags.HideTag(c, uid, []int64{tagHideReq.Id}, tagHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.CategoryHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
@@ -167,7 +167,7 @@ func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (interface{}, *errs
}
// TagMoveHandler moves display order of existed transaction tags by request parameters for current user
func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error) {
var tagMoveReq models.TransactionTagMoveRequest
err := c.ShouldBindJSON(&tagMoveReq)
@@ -190,7 +190,7 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (interface{}, *errs
tags[i] = tag
}
err = a.tags.ModifyTagDisplayOrders(uid, tags)
err = a.tags.ModifyTagDisplayOrders(c, uid, tags)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.CategoryMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
@@ -202,7 +202,7 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (interface{}, *errs
}
// TagDeleteHandler deletes an existed transaction tag by request parameters for current user
func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (any, *errs.Error) {
var tagDeleteReq models.TransactionTagDeleteRequest
err := c.ShouldBindJSON(&tagDeleteReq)
@@ -212,7 +212,7 @@ func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (interface{}, *er
}
uid := c.GetCurrentUid()
err = a.tags.DeleteTag(uid, tagDeleteReq.Id)
err = a.tags.DeleteTag(c, uid, tagDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagDeleteHandler] failed to delete tag \"id:%d\" for user \"uid:%d\", because %s", tagDeleteReq.Id, uid, err.Error())
@@ -223,7 +223,7 @@ func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (interface{}, *er
return true, nil
}
func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.TransactionTagCreateRequest, order int) *models.TransactionTag {
func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.TransactionTagCreateRequest, order int32) *models.TransactionTag {
return &models.TransactionTag{
Uid: uid,
Name: tagCreateReq.Name,
+163 -77
View File
@@ -4,6 +4,8 @@ import (
"sort"
"strings"
orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
@@ -35,7 +37,7 @@ var (
)
// TransactionCountHandler returns transaction total count of current user
func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (any, *errs.Error) {
var transactionCountReq models.TransactionCountRequest
err := c.ShouldBindQuery(&transactionCountReq)
@@ -46,14 +48,26 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{},
uid := c.GetCurrentUid()
allCategoryIds, err := a.getCategoryAndSubCategoryIds(transactionCountReq.CategoryId, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionCountReq.AccountId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionCountReq.CategoryId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get transaction category error, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
totalCount, err := a.transactions.GetTransactionCount(uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, transactionCountReq.AccountId, transactionCountReq.Keyword)
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, transactionCountReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
countResp := &models.TransactionCountResponse{
TotalCount: totalCount,
@@ -63,7 +77,7 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{},
}
// TransactionListHandler returns transaction list of current user
func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionListHandler(c *core.Context) (any, *errs.Error) {
var transactionListReq models.TransactionListByMaxTimeRequest
err := c.ShouldBindQuery(&transactionListReq)
@@ -80,7 +94,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -90,24 +104,42 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
return nil, errs.ErrUserNotFound
}
allCategoryIds, err := a.getCategoryAndSubCategoryIds(transactionListReq.CategoryId, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get transaction category error, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions, err := a.transactions.GetTransactionsByMaxTime(uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, transactionListReq.AccountId, transactionListReq.Keyword, transactionListReq.Count+1, true)
var totalCount int64
if transactionListReq.WithCount {
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
hasMore := false
var nextTimeSequenceId *int64
if len(transactions) > transactionListReq.Count {
if len(transactions) > int(transactionListReq.Count) {
hasMore = true
nextTimeSequenceId = &transactions[transactionListReq.Count].TransactionTime
transactions = transactions[:transactionListReq.Count]
@@ -128,11 +160,15 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
transactionResps.NextTimeSequenceId = nextTimeSequenceId
}
if transactionListReq.WithCount {
transactionResps.TotalCount = &totalCount
}
return transactionResps, nil
}
// TransactionMonthListHandler returns transaction list of current user by month
func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interface{}, *errs.Error) {
// TransactionMonthListHandler returns all transaction list of current user by month
func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (any, *errs.Error) {
var transactionListReq models.TransactionListInMonthByPageRequest
err := c.ShouldBindQuery(&transactionListReq)
@@ -149,7 +185,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -159,25 +195,25 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
return nil, errs.ErrUserNotFound
}
allCategoryIds, err := a.getCategoryAndSubCategoryIds(transactionListReq.CategoryId, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get transaction category error, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, transactionListReq.AccountId, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, utcOffset)
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalCount, err := a.transactions.GetMonthTransactionCount(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, transactionListReq.AccountId, transactionListReq.Keyword, utcOffset)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthListHandler] failed to get transaction count in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionListResult(c, user, transactions, utcOffset, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
@@ -189,14 +225,14 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
transactionResps := &models.TransactionInfoPageWrapperResponse2{
Items: transactionResult,
TotalCount: totalCount,
TotalCount: int64(transactionResult.Len()),
}
return transactionResps, nil
}
// TransactionStatisticsHandler returns transaction statistics of current user
func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (any, *errs.Error) {
var statisticReq models.TransactionStatisticRequest
err := c.ShouldBindQuery(&statisticReq)
@@ -206,7 +242,12 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interfa
}
uid := c.GetCurrentUid()
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(uid, statisticReq.StartTime, statisticReq.EndTime)
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticResp := &models.TransactionStatisticResponse{
StartTime: statisticReq.StartTime,
@@ -228,7 +269,7 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interfa
}
// TransactionAmountsHandler returns transaction amounts of current user
func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs.Error) {
var transactionAmountsReq models.TransactionAmountsRequest
err := c.ShouldBindQuery(&transactionAmountsReq)
@@ -249,31 +290,31 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
return nil, errs.ErrQueryItemsEmpty
}
if len(requestItems) > 5 {
if len(requestItems) > 20 {
log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are too many items")
return nil, errs.ErrQueryItemsTooMuch
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(uid)
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
accountMap := a.accounts.GetAccountMapByList(accounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsResp := make(map[string]*models.TransactionAmountsResponseItem)
amountsResp := orderedmap.New[string, *models.TransactionAmountsResponseItem]()
for i := 0; i < len(requestItems); i++ {
requestItem := requestItems[i]
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(uid, requestItem.StartTime, requestItem.EndTime)
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsMap := make(map[string]*models.TransactionAmountsResponseItemAmountInfo)
@@ -322,24 +363,26 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
amountsMap[account.Currency] = totalAmounts
}
allTotalAmounts := make([]*models.TransactionAmountsResponseItemAmountInfo, 0)
allTotalAmounts := make(models.TransactionAmountsResponseItemAmountInfoSlice, 0)
for _, totalAmounts := range amountsMap {
allTotalAmounts = append(allTotalAmounts, totalAmounts)
}
amountsResp[requestItem.Name] = &models.TransactionAmountsResponseItem{
sort.Sort(allTotalAmounts)
amountsResp.Set(requestItem.Name, &models.TransactionAmountsResponseItem{
StartTime: requestItem.StartTime,
EndTime: requestItem.EndTime,
Amounts: allTotalAmounts,
}
})
}
return amountsResp, nil
}
// TransactionMonthAmountsHandler returns every month transaction amounts of current user
func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (any, *errs.Error) {
var transactionAmountsReq models.TransactionMonthAmountsRequest
err := c.ShouldBindQuery(&transactionAmountsReq)
@@ -364,15 +407,21 @@ func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (inter
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(uid)
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
accountMap := a.accounts.GetAccountMapByList(accounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
totalAmounts, err := a.transactions.GetAccountsMonthTotalIncomeAndExpense(c, uid, startTime, endTime, pageCountForLoadTransactionAmounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get accounts month total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
totalAmounts, err := a.transactions.GetAccountsMonthTotalIncomeAndExpense(uid, startTime, endTime, pageCountForLoadTransactionAmounts)
amountsMap := make(map[string]map[string]*models.TransactionAmountsResponseItemAmountInfo)
for yearMonth, monthAccountsAmounts := range totalAmounts {
@@ -426,12 +475,14 @@ func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (inter
continue
}
amounts := make([]*models.TransactionAmountsResponseItemAmountInfo, 0, len(monthTotalAmounts))
amounts := make(models.TransactionAmountsResponseItemAmountInfoSlice, 0, len(monthTotalAmounts))
for _, monthTotalAmount := range monthTotalAmounts {
amounts = append(amounts, monthTotalAmount)
}
sort.Sort(amounts)
amountsResp = append(amountsResp, &models.TransactionMonthAmountsResponseItem{
Year: year,
Month: month,
@@ -445,7 +496,7 @@ func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (inter
}
// TransactionGetHandler returns one specific transaction of current user
func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (any, *errs.Error) {
var transactionGetReq models.TransactionGetRequest
err := c.ShouldBindQuery(&transactionGetReq)
@@ -462,7 +513,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -472,15 +523,15 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionGetReq.Id)
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionGetReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
transaction = a.transactions.GetRelatedTransferTransaction(transaction, transaction.RelatedId)
transaction = a.transactions.GetRelatedTransferTransaction(transaction)
}
accountIds := make([]int64, 0, 2)
@@ -491,7 +542,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
accountIds = utils.ToUniqueInt64Slice(accountIds)
}
accountMap, err := a.accounts.GetAccountsByAccountIds(uid, accountIds)
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, accountIds)
if _, exists := accountMap[transaction.AccountId]; !exists {
log.WarnfWithRequestId(c, "[transactions.TransactionGetHandler] account of transaction \"id:%d\" does not exist for user \"uid:%d\"", transaction.TransactionId, uid)
@@ -505,31 +556,31 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
}
}
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, []int64{transaction.TransactionId})
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, []int64{transaction.TransactionId})
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var category *models.TransactionCategory
var tagMap map[int64]*models.TransactionTag
if !transactionGetReq.TrimCategory {
category, err = a.transactionCategories.GetCategoryByCategoryId(uid, transaction.CategoryId)
category, err = a.transactionCategories.GetCategoryByCategoryId(c, uid, transaction.CategoryId)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions category for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
if !transactionGetReq.TrimTag {
tagMap, err = a.transactionTags.GetTagsByTagIds(uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds)))
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds)))
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
@@ -561,7 +612,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
}
// TransactionCreateHandler saves a new transaction by request parameters for current user
func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs.Error) {
var transactionCreateReq models.TransactionCreateRequest
err := c.ShouldBindJSON(&transactionCreateReq)
@@ -601,7 +652,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -611,14 +662,14 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
return nil, errs.ErrUserNotFound
}
transaction := a.createNewTransactionModel(uid, &transactionCreateReq)
transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
}
err = a.transactions.CreateTransaction(transaction, tagIds)
err = a.transactions.CreateTransaction(c, transaction, tagIds)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionCreateHandler] failed to create transaction \"id:%d\" for user \"uid:%d\", because %s", transaction.TransactionId, uid, err.Error())
@@ -633,7 +684,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
}
// TransactionModifyHandler saves an existed transaction by request parameters for current user
func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (any, *errs.Error) {
var transactionModifyReq models.TransactionModifyRequest
err := c.ShouldBindJSON(&transactionModifyReq)
@@ -650,7 +701,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -660,7 +711,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionModifyReq.Id)
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error())
@@ -672,11 +723,11 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
return nil, errs.ErrTransactionTypeInvalid
}
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, []int64{transaction.TransactionId})
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, []int64{transaction.TransactionId})
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
@@ -702,6 +753,11 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
newTransaction.RelatedAccountAmount = transactionModifyReq.DestinationAmount
}
if transactionModifyReq.GeoLocation != nil {
newTransaction.GeoLongitude = transactionModifyReq.GeoLocation.Longitude
newTransaction.GeoLatitude = transactionModifyReq.GeoLocation.Latitude
}
if newTransaction.CategoryId == transaction.CategoryId &&
utils.GetUnixTimeFromTransactionTime(newTransaction.TransactionTime) == utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) &&
newTransaction.TimezoneUtcOffset == transaction.TimezoneUtcOffset &&
@@ -711,6 +767,8 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
(transaction.Type != models.TRANSACTION_DB_TYPE_TRANSFER_OUT || newTransaction.RelatedAccountAmount == transaction.RelatedAccountAmount) &&
newTransaction.HideAmount == transaction.HideAmount &&
newTransaction.Comment == transaction.Comment &&
newTransaction.GeoLongitude == transaction.GeoLongitude &&
newTransaction.GeoLatitude == transaction.GeoLatitude &&
utils.Int64SliceEquals(tagIds, transactionTagIds) {
return nil, errs.ErrNothingWillBeUpdated
}
@@ -730,7 +788,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
}
err = a.transactions.ModifyTransaction(newTransaction, addTransactionTagIds, removeTransactionTagIds)
err = a.transactions.ModifyTransaction(c, newTransaction, addTransactionTagIds, removeTransactionTagIds)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to update transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error())
@@ -746,7 +804,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
}
// TransactionDeleteHandler deletes an existed transaction by request parameters for current user
func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (any, *errs.Error) {
var transactionDeleteReq models.TransactionDeleteRequest
err := c.ShouldBindJSON(&transactionDeleteReq)
@@ -763,7 +821,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -773,7 +831,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionDeleteReq.Id)
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionDeleteHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionDeleteReq.Id, uid, err.Error())
@@ -791,7 +849,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}
return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime
}
err = a.transactions.DeleteTransaction(uid, transactionDeleteReq.Id)
err = a.transactions.DeleteTransaction(c, uid, transactionDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionDeleteHandler] failed to delete transaction \"id:%d\" for user \"uid:%d\", because %s", transactionDeleteReq.Id, uid, err.Error())
@@ -826,11 +884,33 @@ func (a *TransactionsApi) filterTransactions(c *core.Context, uid int64, transac
return finalTransactions
}
func (a *TransactionsApi) getCategoryAndSubCategoryIds(categoryId int64, uid int64) ([]int64, error) {
func (a *TransactionsApi) getAccountOrSubAccountIds(c *core.Context, accountId int64, uid int64) ([]int64, error) {
var allAccountIds []int64
if accountId > 0 {
allSubAccounts, err := a.accounts.GetSubAccountsByAccountId(c, uid, accountId)
if err != nil {
return nil, err
}
if len(allSubAccounts) > 0 {
for i := 0; i < len(allSubAccounts); i++ {
allAccountIds = append(allAccountIds, allSubAccounts[i].AccountId)
}
} else {
allAccountIds = append(allAccountIds, accountId)
}
}
return allAccountIds, nil
}
func (a *TransactionsApi) getCategoryOrSubCategoryIds(c *core.Context, categoryId int64, uid int64) ([]int64, error) {
var allCategoryIds []int64
if categoryId > 0 {
allSubCategories, err := a.transactionCategories.GetAllCategoriesByUid(uid, 0, categoryId)
allSubCategories, err := a.transactionCategories.GetAllCategoriesByUid(c, uid, 0, categoryId)
if err != nil {
return nil, err
@@ -897,7 +977,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
categoryIds = append(categoryIds, transactions[i].CategoryId)
}
allAccounts, err := a.accounts.GetAccountsByAccountIds(uid, utils.ToUniqueInt64Slice(accountIds))
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds))
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
@@ -906,7 +986,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
transactions = a.filterTransactions(c, uid, transactions, allAccounts)
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, transactionIds)
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
@@ -917,7 +997,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
var tagMap map[int64]*models.TransactionTag
if !trimCategory {
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(uid, utils.ToUniqueInt64Slice(categoryIds))
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds))
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error())
@@ -926,7 +1006,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
}
if !trimTag {
tagMap, err = a.transactionTags.GetTagsByTagIds(uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds)))
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds)))
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
@@ -940,7 +1020,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
transaction = a.transactions.GetRelatedTransferTransaction(transaction, transaction.RelatedId)
transaction = a.transactions.GetRelatedTransferTransaction(transaction)
}
transactionEditable := transaction.IsEditable(user, utcOffset, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId])
@@ -973,7 +1053,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
return result, nil
}
func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreateReq *models.TransactionCreateRequest) *models.Transaction {
func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreateReq *models.TransactionCreateRequest, clientIp string) *models.Transaction {
var transactionDbType models.TransactionDbType
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE {
@@ -996,6 +1076,7 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
Amount: transactionCreateReq.SourceAmount,
HideAmount: transactionCreateReq.HideAmount,
Comment: transactionCreateReq.Comment,
CreatedIp: clientIp,
}
if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER {
@@ -1003,5 +1084,10 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
transaction.RelatedAccountAmount = transactionCreateReq.DestinationAmount
}
if transactionCreateReq.GeoLocation != nil {
transaction.GeoLongitude = transactionCreateReq.GeoLocation.Longitude
transaction.GeoLatitude = transactionCreateReq.GeoLocation.Latitude
}
return transaction
}
+23 -22
View File
@@ -32,9 +32,9 @@ var (
)
// TwoFactorStatusHandler returns 2fa status of current user
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
if err == errs.ErrTwoFactorIsNotEnabled {
statusResp := &models.TwoFactorStatusResponse{
@@ -58,9 +58,9 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (in
}
// TwoFactorEnableRequestHandler returns a new 2fa secret and qr code for current user to set 2fa and verify passcode next
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two factor setting, because %s", err.Error())
@@ -71,7 +71,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
return nil, errs.ErrTwoFactorAlreadyEnabled
}
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -81,7 +81,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
return nil, errs.ErrUserNotFound
}
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(user)
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor secret, because %s", err.Error())
@@ -110,7 +110,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
}
// TwoFactorEnableConfirmHandler enables 2fa for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Context) (any, *errs.Error) {
var confirmReq models.TwoFactorEnableConfirmRequest
err := c.ShouldBindJSON(&confirmReq)
@@ -120,7 +120,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
}
uid := c.GetCurrentUid()
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two factor setting, because %s", err.Error())
@@ -131,7 +131,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
return nil, errs.ErrTwoFactorAlreadyEnabled
}
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -158,14 +158,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(uid, recoveryCodes, user.Salt)
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(twoFactorSetting)
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(c, twoFactorSetting)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor setting for user \"uid:%d\", because %s", uid, err.Error())
@@ -175,7 +175,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two factor authorization", uid)
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(uid, now)
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
if err == nil {
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
@@ -183,7 +183,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -195,6 +195,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
return confirmResp, nil
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
@@ -208,7 +209,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
}
// TwoFactorDisableHandler disables 2fa for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (any, *errs.Error) {
var disableReq models.TwoFactorDisableRequest
err := c.ShouldBindJSON(&disableReq)
@@ -218,7 +219,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -232,7 +233,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
return nil, errs.ErrUserPasswordWrong
}
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two factor setting, because %s", err.Error())
@@ -243,14 +244,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
return nil, errs.ErrTwoFactorIsNotEnabled
}
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor recovery codes for user \"uid:%d\"", uid)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(uid)
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor setting for user \"uid:%d\"", uid)
@@ -263,7 +264,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
}
// TwoFactorRecoveryCodeRegenerateHandler returns new 2fa recovery codes and revokes old recovery codes for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (any, *errs.Error) {
var regenerateReq models.TwoFactorRegenerateRecoveryCodeRequest
err := c.ShouldBindJSON(&regenerateReq)
@@ -273,7 +274,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -287,7 +288,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
return nil, errs.ErrUserPasswordWrong
}
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two factor setting, because %s", err.Error())
@@ -305,7 +306,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(uid, recoveryCodes, user.Salt)
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
+305 -23
View File
@@ -4,6 +4,8 @@ import (
"strings"
"time"
"github.com/gin-gonic/gin/binding"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
@@ -15,26 +17,28 @@ import (
// UsersApi represents user api
type UsersApi struct {
users *services.UserService
tokens *services.TokenService
users *services.UserService
tokens *services.TokenService
accounts *services.AccountService
}
// Initialize a user api singleton instance
var (
Users = &UsersApi{
users: services.Users,
tokens: services.Tokens,
users: services.Users,
tokens: services.Tokens,
accounts: services.Accounts,
}
)
// UserRegisterHandler saves a new user by request parameters
func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
if !settings.Container.Current.EnableUserRegister {
return nil, errs.ErrUserRegistrationNotAllowed
}
var userRegisterReq models.UserRegisterRequest
err := c.ShouldBindJSON(&userRegisterReq)
err := c.ShouldBindBodyWith(&userRegisterReq, binding.JSON)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
@@ -55,12 +59,13 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
Email: userRegisterReq.Email,
Nickname: userRegisterReq.Nickname,
Password: userRegisterReq.Password,
Language: userRegisterReq.Language,
DefaultCurrency: userRegisterReq.DefaultCurrency,
FirstDayOfWeek: userRegisterReq.FirstDayOfWeek,
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
}
err = a.users.CreateUser(user)
err = a.users.CreateUser(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
@@ -69,12 +74,46 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
authResp := &models.AuthResponse{
Need2FA: false,
User: user.ToUserBasicInfo(),
presetCategoriesSaved := false
if len(userRegisterReq.Categories) > 0 {
_, err = TransactionCategories.createBatchCategories(c, user.Uid, &userRegisterReq.TransactionCategoryCreateBatchRequest)
if err == nil {
presetCategoriesSaved = true
}
}
token, claims, err := a.tokens.CreateToken(user, c)
authResp := &models.RegisterResponse{
AuthResponse: models.AuthResponse{
Need2FA: false,
User: user.ToUserBasicInfo(),
},
NeedVerifyEmail: settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableUserForceVerifyEmail,
PresetCategoriesSaved: presetCategoriesSaved,
}
if settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP {
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
} else {
go func() {
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
}
}()
}
}
if settings.Container.Current.EnableUserForceVerifyEmail {
return authResp, nil
}
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -82,6 +121,7 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
}
authResp.Token = token
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
@@ -89,10 +129,72 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
return authResp, nil
}
// UserProfileHandler returns user profile of current user
func (a *UsersApi) UserProfileHandler(c *core.Context) (interface{}, *errs.Error) {
// UserEmailVerifyHandler sets user email address verified
func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
var userVerifyEmailReq models.UserVerifyEmailRequest
err := c.ShouldBindJSON(&userVerifyEmailReq)
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if user.EmailVerified {
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" email has been verified", user.Uid)
return nil, errs.ErrEmailIsVerified
}
err = a.users.SetUserEmailVerified(c, user.Username)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to update user \"uid:%d\" email address verified, because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
if err == nil {
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] revoke old email verify tokens for user \"uid:%d\"", user.Uid)
} else {
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
resp := &models.UserVerifyEmailResponse{}
if userVerifyEmailReq.RequestNewToken {
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return resp, nil
}
resp.NewToken = token
resp.User = user.ToUserBasicInfo()
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
}
return resp, nil
}
// UserProfileHandler returns user profile of current user
func (a *UsersApi) UserProfileHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -107,7 +209,7 @@ func (a *UsersApi) UserProfileHandler(c *core.Context) (interface{}, *errs.Error
}
// UserUpdateProfileHandler saves user profile by request parameters for current user
func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error) {
var userUpdateReq models.UserProfileUpdateRequest
err := c.ShouldBindJSON(&userUpdateReq)
@@ -117,7 +219,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -159,6 +261,35 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
anythingUpdate = true
}
if userUpdateReq.DefaultAccountId > 0 && userUpdateReq.DefaultAccountId != user.DefaultAccountId {
accounts, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
if err != nil || len(accounts) < 1 {
return nil, errs.Or(err, errs.ErrUserDefaultAccountIsInvalid)
}
user.DefaultAccountId = userUpdateReq.DefaultAccountId
userNew.DefaultAccountId = userUpdateReq.DefaultAccountId
anythingUpdate = true
}
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
anythingUpdate = true
} else {
userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID
}
modifyUserLanguage := false
if userUpdateReq.Language != user.Language {
user.Language = userUpdateReq.Language
userNew.Language = userUpdateReq.Language
modifyUserLanguage = true
anythingUpdate = true
}
if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency {
user.DefaultCurrency = userUpdateReq.DefaultCurrency
userNew.DefaultCurrency = userUpdateReq.DefaultCurrency
@@ -173,34 +304,84 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
userNew.FirstDayOfWeek = models.WEEKDAY_INVALID
}
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat {
user.LongDateFormat = *userUpdateReq.LongDateFormat
userNew.LongDateFormat = *userUpdateReq.LongDateFormat
anythingUpdate = true
} else {
userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID
userNew.LongDateFormat = models.LONG_DATE_FORMAT_INVALID
}
if userUpdateReq.ShortDateFormat != nil && *userUpdateReq.ShortDateFormat != user.ShortDateFormat {
user.ShortDateFormat = *userUpdateReq.ShortDateFormat
userNew.ShortDateFormat = *userUpdateReq.ShortDateFormat
anythingUpdate = true
} else {
userNew.ShortDateFormat = models.SHORT_DATE_FORMAT_INVALID
}
if userUpdateReq.LongTimeFormat != nil && *userUpdateReq.LongTimeFormat != user.LongTimeFormat {
user.LongTimeFormat = *userUpdateReq.LongTimeFormat
userNew.LongTimeFormat = *userUpdateReq.LongTimeFormat
anythingUpdate = true
} else {
userNew.LongTimeFormat = models.LONG_TIME_FORMAT_INVALID
}
if userUpdateReq.ShortTimeFormat != nil && *userUpdateReq.ShortTimeFormat != user.ShortTimeFormat {
user.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
userNew.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
anythingUpdate = true
} else {
userNew.ShortTimeFormat = models.SHORT_TIME_FORMAT_INVALID
}
if !anythingUpdate {
return nil, errs.ErrNothingWillBeUpdated
}
keyProfileUpdated, err := a.users.UpdateUser(userNew)
keyProfileUpdated, emailSetToUnverified, err := a.users.UpdateUser(c, userNew, modifyUserLanguage)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if emailSetToUnverified {
user.EmailVerified = false
}
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
resp := &models.UserProfileUpdateResponse{
User: user.ToUserBasicInfo(),
}
if emailSetToUnverified && settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP {
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
} else {
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
} else {
go func() {
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
}
}()
}
}
}
if keyProfileUpdated {
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(uid, now)
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
if err == nil {
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
@@ -208,7 +389,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -216,6 +397,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
}
resp.NewToken = token
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
@@ -225,3 +407,103 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
return resp, nil
}
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
if !settings.Container.Current.EnableUserVerifyEmail {
return nil, errs.ErrEmailValidationNotAllowed
}
var userResendVerifyEmailReq models.UserResendVerifyEmailRequest
err := c.ShouldBindJSON(&userResendVerifyEmailReq)
user, err := a.users.GetUserByEmail(c, userResendVerifyEmailReq.Email)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if !a.users.IsPasswordEqualsUserPassword(userResendVerifyEmailReq.Password, user) {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] request password not equals to the user password")
return nil, errs.ErrUserPasswordWrong
}
if user.Disabled {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if user.EmailVerified {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" email has been verified", user.Uid)
return nil, errs.ErrEmailIsVerified
}
if !settings.Container.Current.EnableSMTP {
return nil, errs.ErrSMTPServerNotEnabled
}
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
go func() {
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
}
}()
return true, nil
}
// UserSendVerifyEmailByLoginedUserHandler sends logined user verify email
func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any, *errs.Error) {
if !settings.Container.Current.EnableUserVerifyEmail {
return nil, errs.ErrEmailValidationNotAllowed
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.EmailVerified {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] user \"uid:%d\" email has been verified", user.Uid)
return nil, errs.ErrEmailIsVerified
}
if !settings.Container.Current.EnableSMTP {
return nil, errs.ErrSMTPServerNotEnabled
}
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
go func() {
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
}
}()
return true, nil
}
+164 -17
View File
@@ -10,6 +10,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
@@ -26,6 +27,7 @@ type UserDataCli struct {
users *services.UserService
twoFactorAuthorizations *services.TwoFactorAuthorizationService
tokens *services.TokenService
forgetPasswords *services.ForgetPasswordService
}
// Initialize an user data cli singleton instance
@@ -39,6 +41,7 @@ var (
users: services.Users,
twoFactorAuthorizations: services.TwoFactorAuthorizations,
tokens: services.Tokens,
forgetPasswords: services.ForgetPasswords,
}
)
@@ -84,7 +87,7 @@ func (l *UserDataCli) AddNewUser(c *cli.Context, username string, email string,
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
}
err := l.users.CreateUser(user)
err := l.users.CreateUser(nil, user)
if err != nil {
log.BootErrorf("[user_data.AddNewUser] failed to create user \"%s\", because %s", user.Username, err.Error())
@@ -103,7 +106,7 @@ func (l *UserDataCli) GetUserByUsername(c *cli.Context, username string) (*model
return nil, errs.ErrUsernameIsEmpty
}
user, err := l.users.GetUserByUsername(username)
user, err := l.users.GetUserByUsername(nil, username)
if err != nil {
log.BootErrorf("[user_data.GetUserByUsername] failed to get user by user name \"%s\", because %s", username, err.Error())
@@ -125,7 +128,7 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
return errs.ErrPasswordIsEmpty
}
user, err := l.users.GetUserByUsername(username)
user, err := l.users.GetUserByUsername(nil, username)
if err != nil {
log.BootErrorf("[user_data.ModifyUserPassword] failed to get user by user name \"%s\", because %s", username, err.Error())
@@ -142,7 +145,7 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
Password: password,
}
_, err = l.users.UpdateUser(userNew)
_, _, err = l.users.UpdateUser(nil, userNew, false)
if err != nil {
log.BootErrorf("[user_data.ModifyUserPassword] failed to update user \"%s\" password, because %s", user.Username, err.Error())
@@ -150,7 +153,7 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
}
now := time.Now().Unix()
err = l.tokens.DeleteTokensBeforeTime(user.Uid, now)
err = l.tokens.DeleteTokensBeforeTime(nil, user.Uid, now)
if err == nil {
log.BootInfof("[user_data.ModifyUserPassword] revoke old tokens before unix time \"%d\" for user \"%s\"", now, user.Username)
@@ -161,6 +164,150 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
return nil
}
// SendPasswordResetMail sends an email with password reset link
func (l *UserDataCli) SendPasswordResetMail(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.SendPasswordResetMail] user name is empty")
return errs.ErrUsernameIsEmpty
}
user, err := l.users.GetUserByUsername(nil, username)
if err != nil {
log.BootErrorf("[user_data.SendPasswordResetMail] failed to get user by user name \"%s\", because %s", username, err.Error())
return err
}
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
log.BootWarnf("[user_data.SendPasswordResetMail] user \"uid:%d\" has not verified email", user.Uid)
return errs.ErrEmailIsNotVerified
}
token, _, err := l.tokens.CreatePasswordResetToken(nil, user)
if err != nil {
log.BootErrorf("[user_data.SendPasswordResetMail] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return err
}
err = l.forgetPasswords.SendPasswordResetEmail(nil, user, token, "")
if err != nil {
log.BootWarnf("[user_data.SendPasswordResetMail] cannot send email to \"%s\", because %s", user.Email, err.Error())
return err
}
return nil
}
// EnableUser sets user enabled according to the specified user name
func (l *UserDataCli) EnableUser(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.EnableUser] user name is empty")
return errs.ErrUsernameIsEmpty
}
err := l.users.EnableUser(nil, username)
if err != nil {
log.BootErrorf("[user_data.EnableUser] failed to set user enabled by user name \"%s\", because %s", username, err.Error())
return err
}
return nil
}
// DisableUser sets user disabled according to the specified user name
func (l *UserDataCli) DisableUser(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.DisableUser] user name is empty")
return errs.ErrUsernameIsEmpty
}
err := l.users.DisableUser(nil, username)
if err != nil {
log.BootErrorf("[user_data.DisableUser] failed to set user disabled by user name \"%s\", because %s", username, err.Error())
return err
}
return nil
}
// ResendVerifyEmail resends an email with account activation link
func (l *UserDataCli) ResendVerifyEmail(c *cli.Context, username string) error {
if !settings.Container.Current.EnableUserVerifyEmail {
return errs.ErrEmailValidationNotAllowed
}
if username == "" {
log.BootErrorf("[user_data.ResendVerifyEmail] user name is empty")
return errs.ErrUsernameIsEmpty
}
user, err := l.users.GetUserByUsername(nil, username)
if err != nil {
log.BootErrorf("[user_data.ResendVerifyEmail] failed to get user by user name \"%s\", because %s", username, err.Error())
return err
}
if user.EmailVerified {
log.BootWarnf("[user_data.ResendVerifyEmail] user \"uid:%d\" email has been verified", user.Uid)
return errs.ErrEmailIsVerified
}
token, _, err := l.tokens.CreateEmailVerifyToken(nil, user)
if err != nil {
log.BootErrorf("[user_data.ResendVerifyEmail] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return errs.ErrTokenGenerating
}
err = l.users.SendVerifyEmail(user, token, "")
if err != nil {
log.BootErrorf("[user_data.ResendVerifyEmail] cannot send email to \"%s\", because %s", user.Email, err.Error())
return err
}
return nil
}
// SetUserEmailVerified sets user email address verified
func (l *UserDataCli) SetUserEmailVerified(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.SetUserEmailVerified] user name is empty")
return errs.ErrUsernameIsEmpty
}
err := l.users.SetUserEmailVerified(nil, username)
if err != nil {
log.BootErrorf("[user_data.SetUserEmailVerified] failed to set user email address verified by user name \"%s\", because %s", username, err.Error())
return err
}
return nil
}
// SetUserEmailUnverified sets user email address unverified
func (l *UserDataCli) SetUserEmailUnverified(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.SetUserEmailUnverified] user name is empty")
return errs.ErrUsernameIsEmpty
}
err := l.users.SetUserEmailUnverified(nil, username)
if err != nil {
log.BootErrorf("[user_data.SetUserEmailUnverified] failed to set user email address unverified by user name \"%s\", because %s", username, err.Error())
return err
}
return nil
}
// DeleteUser deletes user according to the specified user name
func (l *UserDataCli) DeleteUser(c *cli.Context, username string) error {
if username == "" {
@@ -168,7 +315,7 @@ func (l *UserDataCli) DeleteUser(c *cli.Context, username string) error {
return errs.ErrUsernameIsEmpty
}
err := l.users.DeleteUser(username)
err := l.users.DeleteUser(nil, username)
if err != nil {
log.BootErrorf("[user_data.DeleteUser] failed to delete user by user name \"%s\", because %s", username, err.Error())
@@ -192,7 +339,7 @@ func (l *UserDataCli) ListUserTokens(c *cli.Context, username string) ([]*models
return nil, err
}
tokens, err := l.tokens.GetAllUnexpiredNormalTokensByUid(uid)
tokens, err := l.tokens.GetAllUnexpiredNormalTokensByUid(nil, uid)
if err != nil {
log.BootErrorf("[user_data.ListUserTokens] failed to get tokens of user \"%s\", because %s", username, err.Error())
@@ -217,7 +364,7 @@ func (l *UserDataCli) ClearUserTokens(c *cli.Context, username string) error {
}
now := time.Now().Unix()
err = l.tokens.DeleteTokensBeforeTime(uid, now)
err = l.tokens.DeleteTokensBeforeTime(nil, uid, now)
if err != nil {
log.BootErrorf("[user_data.ClearUserTokens] failed to delete tokens of user \"%s\", because %s", username, err.Error())
@@ -241,7 +388,7 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
return err
}
enableTwoFactor, err := l.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enableTwoFactor, err := l.twoFactorAuthorizations.ExistsTwoFactorSetting(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to check two factor setting, because %s", err.Error())
@@ -252,14 +399,14 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
return errs.ErrTwoFactorIsNotEnabled
}
err = l.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
err = l.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two factor recovery codes for user \"%s\"", username)
return err
}
err = l.twoFactorAuthorizations.DeleteTwoFactorSetting(uid)
err = l.twoFactorAuthorizations.DeleteTwoFactorSetting(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two factor setting for user \"%s\"", username)
@@ -298,7 +445,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
}
}
allTransactions, err := l.transactions.GetAllTransactions(uid, pageCountForGettingTransactions, false)
allTransactions, err := l.transactions.GetAllTransactions(nil, uid, pageCountForGettingTransactions, false)
if err != nil {
log.BootErrorf("[user_data.CheckTransactionAndAccount] failed to all transactions for user \"%s\", because %s", username, err.Error())
@@ -410,7 +557,7 @@ func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte
return nil, err
}
allTransactions, err := l.transactions.GetAllTransactions(uid, pageCountForDataExport, true)
allTransactions, err := l.transactions.GetAllTransactions(nil, uid, pageCountForDataExport, true)
if err != nil {
log.BootErrorf("[user_data.ExportTransaction] failed to all transactions for user \"%s\", because %s", username, err.Error())
@@ -444,7 +591,7 @@ func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountM
return nil, nil, nil, nil, errs.ErrUserIdInvalid
}
accounts, err := l.accounts.GetAllAccountsByUid(uid)
accounts, err := l.accounts.GetAllAccountsByUid(nil, uid)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get accounts for user \"%s\", because %s", username, err.Error())
@@ -453,7 +600,7 @@ func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountM
accountMap = l.accounts.GetAccountMapByList(accounts)
categories, err := l.categories.GetAllCategoriesByUid(uid, 0, -1)
categories, err := l.categories.GetAllCategoriesByUid(nil, uid, 0, -1)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get categories for user \"%s\", because %s", username, err.Error())
@@ -462,7 +609,7 @@ func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountM
categoryMap = l.categories.GetCategoryMapByList(categories)
tags, err := l.tags.GetAllTagsByUid(uid)
tags, err := l.tags.GetAllTagsByUid(nil, uid)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get tags for user \"%s\", because %s", username, err.Error())
@@ -471,7 +618,7 @@ func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountM
tagMap = l.tags.GetTagMapByList(tags)
tagIndexs, err = l.tags.GetAllTagIdsOfAllTransactions(uid)
tagIndexs, err = l.tags.GetAllTagIdsOfAllTransactions(nil, uid)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get tag index for user \"%s\", because %s", username, err.Error())
+27 -6
View File
@@ -9,9 +9,13 @@ import (
)
const requestIdFieldKey = "REQUEST_ID"
const textualTokenFieldKey = "TOKEN_STRING"
const tokenClaimsFieldKey = "TOKEN_CLAIMS"
const responseErrorFieldKey = "RESPONSE_ERROR"
// AcceptLanguageHeaderName represents the header name of accept language
const AcceptLanguageHeaderName = "Accept-Language"
// ClientTimezoneOffsetHeaderName represents the header name of client timezone offset
const ClientTimezoneOffsetHeaderName = "X-Timezone-Offset"
@@ -37,7 +41,23 @@ func (c *Context) GetRequestId() string {
return requestId.(string)
}
// SetTokenClaims sets the given user token id to context
// SetTextualToken sets the given user token to context
func (c *Context) SetTextualToken(token string) {
c.Set(textualTokenFieldKey, token)
}
// GetTextualToken returns the current user textual token
func (c *Context) GetTextualToken() string {
token, exists := c.Get(textualTokenFieldKey)
if !exists {
return ""
}
return token.(string)
}
// SetTokenClaims sets the given user token to context
func (c *Context) SetTokenClaims(claims *UserTokenClaims) {
c.Set(tokenClaimsFieldKey, claims)
}
@@ -61,13 +81,14 @@ func (c *Context) GetCurrentUid() int64 {
return 0
}
uid, err := strconv.ParseInt(claims.Id, 10, 64)
return claims.Uid
}
if err != nil {
return 0
}
// GetClientLocale returns the client locale name
func (c *Context) GetClientLocale() string {
value := c.GetHeader(AcceptLanguageHeaderName)
return uid
return value
}
// GetClientTimezoneOffset returns the client timezone offset
+9 -2
View File
@@ -1,12 +1,19 @@
package core
import "github.com/mayswind/ezbookkeeping/pkg/errs"
import (
"net/http/httputil"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
// MiddlewareHandlerFunc represents the middleware handler function
type MiddlewareHandlerFunc func(*Context)
// ApiHandlerFunc represents the api handler function
type ApiHandlerFunc func(*Context) (interface{}, *errs.Error)
type ApiHandlerFunc func(*Context) (any, *errs.Error)
// DataHandlerFunc represents the handler function that returns byte array
type DataHandlerFunc func(*Context) ([]byte, string, *errs.Error)
// ProxyHandlerFunc represents the reverse proxy handler function
type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
+44 -4
View File
@@ -1,7 +1,9 @@
package core
import (
"github.com/dgrijalva/jwt-go"
"time"
"github.com/golang-jwt/jwt/v5"
)
// TokenType represents token type
@@ -9,14 +11,52 @@ type TokenType byte
// Token types
const (
USER_TOKEN_TYPE_NORMAL TokenType = 1
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
USER_TOKEN_TYPE_NORMAL TokenType = 1
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
USER_TOKEN_TYPE_EMAIL_VERIFY TokenType = 3
USER_TOKEN_TYPE_PASSWORD_RESET TokenType = 4
)
// UserTokenClaims represents user token
type UserTokenClaims struct {
UserTokenId string `json:"userTokenId"`
Uid int64 `json:"jti,string"`
Username string `json:"username,omitempty"`
Type TokenType `json:"type"`
jwt.StandardClaims
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
}
// GetExpirationTime returns the expiration time of this token
func (c *UserTokenClaims) GetExpirationTime() (*jwt.NumericDate, error) {
return &jwt.NumericDate{
Time: time.Unix(c.ExpiresAt, 0),
}, nil
}
// GetIssuedAt returns the issue time of this token
func (c *UserTokenClaims) GetIssuedAt() (*jwt.NumericDate, error) {
return &jwt.NumericDate{
Time: time.Unix(c.IssuedAt, 0),
}, nil
}
// GetNotBefore returns the earliest valid time of this token
func (c *UserTokenClaims) GetNotBefore() (*jwt.NumericDate, error) {
return &jwt.NumericDate{}, nil
}
// GetIssuer returns the issuer of this token
func (c *UserTokenClaims) GetIssuer() (string, error) {
return "", nil
}
// GetSubject returns the subject of this token
func (c *UserTokenClaims) GetSubject() (string, error) {
return "", nil
}
// GetAudience returns the audience of this token
func (c *UserTokenClaims) GetAudience() (jwt.ClaimStrings, error) {
return jwt.ClaimStrings{}, nil
}
+18 -4
View File
@@ -1,15 +1,29 @@
package datastore
import "xorm.io/xorm"
import (
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// Database represents a database instance
type Database struct {
*xorm.EngineGroup
engineGroup *xorm.EngineGroup
}
// NewSession starts a new session with the specified context
func (db *Database) NewSession(c *core.Context) *xorm.Session {
return db.engineGroup.Context(NewXOrmContextAdapter(c))
}
// DoTransaction runs a new database transaction
func (db *Database) DoTransaction(fn func(sess *xorm.Session) error) (err error) {
sess := db.NewSession()
func (db *Database) DoTransaction(c *core.Context, fn func(sess *xorm.Session) error) (err error) {
sess := db.engineGroup.NewSession()
if c != nil {
sess.Context(NewXOrmContextAdapter(c))
}
defer sess.Close()
if err = sess.Begin(); err != nil {
+7 -6
View File
@@ -3,6 +3,7 @@ package datastore
import (
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
@@ -17,21 +18,21 @@ func (s *DataStore) Choose(key int64) *Database {
}
// Query returns a new database session in a specific database by sharding key
func (s *DataStore) Query(key int64) *xorm.Session {
return s.Choose(key).NewSession()
func (s *DataStore) Query(c *core.Context, key int64) *xorm.Session {
return s.Choose(key).NewSession(c)
}
// DoTransaction runs a new database transaction in a specific database by sharding key
func (s *DataStore) DoTransaction(key int64, fn func(sess *xorm.Session) error) (err error) {
return s.Choose(key).DoTransaction(fn)
func (s *DataStore) DoTransaction(key int64, c *core.Context, fn func(sess *xorm.Session) error) (err error) {
return s.Choose(key).DoTransaction(c, fn)
}
// SyncStructs updates database structs by database models
func (s *DataStore) SyncStructs(beans ...interface{}) error {
func (s *DataStore) SyncStructs(beans ...any) error {
var err error
for i := 0; i < len(s.databases); i++ {
err = s.databases[i].Sync2(beans...)
err = s.databases[i].engineGroup.Sync2(beans...)
if err != nil {
return err
+8 -11
View File
@@ -2,6 +2,7 @@ package datastore
import (
"fmt"
"net"
"net/url"
"os"
"strings"
@@ -98,19 +99,19 @@ func initializeDatabase(dbConfig *settings.DatabaseConfig) (*Database, error) {
return nil, err
}
engineGroup.SetMaxIdleConns(dbConfig.MaxIdleConnection)
engineGroup.SetMaxOpenConns(dbConfig.MaxOpenConnection)
engineGroup.SetMaxIdleConns(int(dbConfig.MaxIdleConnection))
engineGroup.SetMaxOpenConns(int(dbConfig.MaxOpenConnection))
engineGroup.SetConnMaxLifetime(time.Duration(dbConfig.ConnectionMaxLifeTime) * time.Second)
return &Database{
EngineGroup: engineGroup,
engineGroup: engineGroup,
}, nil
}
func setDatabaseLogger(database *Database, config *settings.Config) {
if config.EnableQueryLog {
database.SetLogger(NewXOrmLoggerAdapter(config.EnableQueryLog, config.LogLevel))
database.ShowSQL(true)
database.engineGroup.SetLogger(NewXOrmLoggerAdapter(config.EnableQueryLog, config.LogLevel))
database.engineGroup.ShowSQL(true)
}
}
@@ -126,16 +127,12 @@ func getMysqlConnectionString(dbConfig *settings.DatabaseConfig) (string, error)
}
func getPostgresConnectionString(dbConfig *settings.DatabaseConfig) (string, error) {
host, port := "", ""
fields := strings.Split(dbConfig.DatabaseHost, ":")
host, port, err := net.SplitHostPort(dbConfig.DatabaseHost)
if len(fields) != 2 {
if err != nil {
return "", errs.ErrDatabaseHostInvalid
}
host = strings.TrimSpace(fields[0])
port = strings.TrimSpace(fields[1])
if strings.HasPrefix(dbConfig.DatabaseHost, "/") { // unix socket path
return fmt.Sprintf("postgres://%s:%s@:%s/%s?sslmode=%s&host=%s",
url.QueryEscape(dbConfig.DatabaseUser), url.QueryEscape(dbConfig.DatabasePassword), port, dbConfig.DatabaseName, dbConfig.DatabaseSSLMode, host), nil
+50
View File
@@ -0,0 +1,50 @@
package datastore
import (
"fmt"
"time"
"xorm.io/xorm/log"
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// XOrmContextAdapter represents the context adapter for xorm
type XOrmContextAdapter struct {
requestId string
}
// Deadline does nothing
func (c *XOrmContextAdapter) Deadline() (deadline time.Time, ok bool) {
return
}
// Done always returns nil
func (c *XOrmContextAdapter) Done() <-chan struct{} {
return nil
}
// Err always returns nil
func (c *XOrmContextAdapter) Err() error {
return nil
}
// Value returns the value associated with this context for key, or nil
// if no value is associated with key.
func (c *XOrmContextAdapter) Value(key any) any {
if key == log.SessionIDKey && c.requestId != "" {
return fmt.Sprintf("r=%s", c.requestId)
}
return nil
}
func NewXOrmContextAdapter(c *core.Context) *XOrmContextAdapter {
if c != nil {
return &XOrmContextAdapter{
requestId: c.GetRequestId(),
}
}
return &XOrmContextAdapter{}
}
+8 -8
View File
@@ -14,42 +14,42 @@ type XOrmLoggerAdapter struct {
}
// Debug logs debug log
func (logger XOrmLoggerAdapter) Debug(v ...interface{}) {
func (logger XOrmLoggerAdapter) Debug(v ...any) {
log.SqlQuery(v...)
}
// Debugf logs debug log with custom format
func (logger XOrmLoggerAdapter) Debugf(format string, v ...interface{}) {
func (logger XOrmLoggerAdapter) Debugf(format string, v ...any) {
log.SqlQueryf(format, v...)
}
// Info logs info log
func (logger XOrmLoggerAdapter) Info(v ...interface{}) {
func (logger XOrmLoggerAdapter) Info(v ...any) {
log.SqlQuery(v...)
}
// Infof logs info log with custom format
func (logger XOrmLoggerAdapter) Infof(format string, v ...interface{}) {
func (logger XOrmLoggerAdapter) Infof(format string, v ...any) {
log.SqlQueryf(format, v...)
}
// Warn logs warn log
func (logger XOrmLoggerAdapter) Warn(v ...interface{}) {
func (logger XOrmLoggerAdapter) Warn(v ...any) {
log.SqlQuery(v...)
}
// Warnf logs warn log with custom format
func (logger XOrmLoggerAdapter) Warnf(format string, v ...interface{}) {
func (logger XOrmLoggerAdapter) Warnf(format string, v ...any) {
log.SqlQueryf(format, v...)
}
// Error logs error log
func (logger XOrmLoggerAdapter) Error(v ...interface{}) {
func (logger XOrmLoggerAdapter) Error(v ...any) {
log.SqlQuery(v...)
}
// Errorf logs error log with custom format
func (logger XOrmLoggerAdapter) Errorf(format string, v ...interface{}) {
func (logger XOrmLoggerAdapter) Errorf(format string, v ...any) {
log.SqlQueryf(format, v...)
}
+23 -8
View File
@@ -1,7 +1,7 @@
package errs
// ErrorCategory represents error category
type ErrorCategory int
type ErrorCategory int32
// Error categories
const (
@@ -14,6 +14,7 @@ const (
SystemSubcategoryDefault = 0
SystemSubcategorySetting = 1
SystemSubcategoryDatabase = 2
SystemSubcategoryMail = 3
)
// Sub categories of normal error
@@ -32,11 +33,12 @@ const (
// Error represents the specific error returned to user
type Error struct {
Category ErrorCategory
SubCategory int
Index int
SubCategory int32
Index int32
HttpStatusCode int
Message string
BaseError []error
Context any
}
// Error returns the error message
@@ -45,12 +47,12 @@ func (err *Error) Error() string {
}
// Code returns the error code
func (err *Error) Code() int {
return int(err.Category)*100000 + err.SubCategory*1000 + err.Index
func (err *Error) Code() int32 {
return int32(err.Category)*100000 + err.SubCategory*1000 + err.Index
}
// New returns a new error instance
func New(category ErrorCategory, subCategory int, index int, httpStatusCode int, message string, baseError ...error) *Error {
func New(category ErrorCategory, subCategory int32, index int32, httpStatusCode int, message string, baseError ...error) *Error {
return &Error{
Category: category,
SubCategory: subCategory,
@@ -62,12 +64,12 @@ func New(category ErrorCategory, subCategory int, index int, httpStatusCode int,
}
// NewSystemError returns a new system error instance
func NewSystemError(subCategory int, index int, httpStatusCode int, message string) *Error {
func NewSystemError(subCategory int32, index int32, httpStatusCode int, message string) *Error {
return New(CATEGORY_SYSTEM, subCategory, index, httpStatusCode, message)
}
// NewNormalError returns a new normal error instance
func NewNormalError(subCategory int, index int, httpStatusCode int, message string) *Error {
func NewNormalError(subCategory int32, index int32, httpStatusCode int, message string) *Error {
return New(CATEGORY_NORMAL, subCategory, index, httpStatusCode, message)
}
@@ -80,6 +82,19 @@ func NewIncompleteOrIncorrectSubmissionError(err error) *Error {
ErrIncompleteOrIncorrectSubmission.Message, err)
}
// NewErrorWithContext returns a new error instance with specified context
func NewErrorWithContext(baseError *Error, context any) *Error {
return &Error{
Category: baseError.Category,
SubCategory: baseError.SubCategory,
Index: baseError.Index,
HttpStatusCode: baseError.HttpStatusCode,
Message: baseError.Message,
BaseError: baseError.BaseError,
Context: context,
}
}
// Or would return the error from err parameter if the this error is defined in this project,
// or return the default error
func Or(err error, defaultErr *Error) *Error {
+9
View File
@@ -0,0 +1,9 @@
package errs
import "net/http"
// Error codes related to mail
var (
ErrSMTPServerNotEnabled = NewSystemError(SystemSubcategoryMail, 0, http.StatusInternalServerError, "SMTP server is not enabled")
ErrSMTPServerHostInvalid = NewSystemError(SystemSubcategoryMail, 1, http.StatusInternalServerError, "SMTP server host is invalid")
)
+7 -5
View File
@@ -4,9 +4,11 @@ import "net/http"
// Error codes related to settings
var (
ErrInvalidProtocol = NewSystemError(SystemSubcategorySetting, 0, http.StatusInternalServerError, "invalid server protocol")
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 1, http.StatusInternalServerError, "invalid log mode")
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "failed to get local address")
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid uuid mode")
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "invalid exchange rates data source")
ErrInvalidProtocol = NewSystemError(SystemSubcategorySetting, 0, http.StatusInternalServerError, "invalid server protocol")
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 1, http.StatusInternalServerError, "invalid log mode")
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "failed to get local address")
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid uuid mode")
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "invalid exchange rates data source")
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid map provider")
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid amap security verification method")
)
+1
View File
@@ -8,4 +8,5 @@ var (
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusNotImplemented, "system is busy")
)
+15 -12
View File
@@ -6,16 +6,19 @@ import (
// Error codes related to tokens
var (
ErrTokenGenerating = NewNormalError(NormalSubcategoryToken, 0, http.StatusInternalServerError, "failed to generate token")
ErrUnauthorizedAccess = NewNormalError(NormalSubcategoryToken, 1, http.StatusUnauthorized, "unauthorized access")
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two factor authorization")
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two factor authorization")
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user token id is invalid")
ErrTokenRecordNotFound = NewNormalError(NormalSubcategoryToken, 10, http.StatusBadRequest, "token is not found")
ErrTokenExpired = NewNormalError(NormalSubcategoryToken, 11, http.StatusBadRequest, "token is expired")
ErrTokenGenerating = NewNormalError(NormalSubcategoryToken, 0, http.StatusInternalServerError, "failed to generate token")
ErrUnauthorizedAccess = NewNormalError(NormalSubcategoryToken, 1, http.StatusUnauthorized, "unauthorized access")
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two factor authorization")
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two factor authorization")
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user token id is invalid")
ErrTokenRecordNotFound = NewNormalError(NormalSubcategoryToken, 10, http.StatusBadRequest, "token is not found")
ErrTokenExpired = NewNormalError(NormalSubcategoryToken, 11, http.StatusBadRequest, "token is expired")
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
ErrEmailVerifyTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 13, http.StatusBadRequest, "email verify token is invalid or expired")
ErrPasswordResetTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 14, http.StatusBadRequest, "password reset token is invalid or expired")
)
+8
View File
@@ -21,4 +21,12 @@ var (
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
ErrEmailIsVerified = NewNormalError(NormalSubcategoryUser, 21, http.StatusBadRequest, "email is verified")
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
)
@@ -33,7 +33,7 @@ type BankOfCanadaExchangeRateData struct {
}
// BankOfCanadaObservationData represents the observation data from bank of Canada
type BankOfCanadaObservationData map[string]interface{}
type BankOfCanadaObservationData map[string]any
// ToLatestExchangeRateResponse returns a view-object according to original data from bank of Canada
func (e *BankOfCanadaExchangeRateData) ToLatestExchangeRateResponse(c *core.Context) *models.LatestExchangeRateResponse {
@@ -62,7 +62,7 @@ func (e *BankOfCanadaExchangeRateData) ToLatestExchangeRateResponse(c *core.Cont
currencyCode := utils.SubString(typeName, 2, 3)
if data, ok := exchangeRateData.(map[string]interface{}); ok {
if data, ok := exchangeRateData.(map[string]any); ok {
exchangeRate := data["v"]
if exchangeRateValue, ok2 := exchangeRate.(string); ok2 {
@@ -18,7 +18,7 @@ const euroCentralBankDataSource = "European Central Bank"
const euroCentralBankBaseCurrency = "EUR"
const euroCentralBankDataUpdateDateFormat = "2006-01-02 15"
const euroCentralBankDataUpdateDateTimezone = "Etc/GMT-1" // UTC+01:00
const euroCentralBankDataUpdateDateTimezone = "Europe/Berlin"
// EuroCentralBankDataSource defines the structure of exchange rates data source of euro central bank
type EuroCentralBankDataSource struct {
@@ -32,6 +32,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error {
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
Container.Current = &NationalBankOfPolandDataSource{}
return nil
} else if config.ExchangeRatesDataSource == settings.MonetaryAuthorityOfSingaporeDataSource {
Container.Current = &MonetaryAuthorityOfSingaporeDataSource{}
return nil
}
return errs.ErrInvalidExchangeRatesDataSource
@@ -0,0 +1,179 @@
package exchangerates
import (
"encoding/json"
"math"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const monetaryAuthorityOfSingaporeExchangeRateUrl = "https://eservices.mas.gov.sg/api/action/datastore/search.json?resource_id=95932927-c8bc-4e7a-b484-68a66a24edfe&sort=end_of_day+desc&limit=1"
const monetaryAuthorityOfSingaporeExchangeRateReferenceUrl = "https://eservices.mas.gov.sg/Statistics/msb/ExchangeRates.aspx"
const monetaryAuthorityOfSingaporeDataSource = "Monetary Authority of Singapore"
const monetaryAuthorityOfSingaporeBaseCurrency = "SGD"
const monetaryAuthorityOfSingaporeDataUpdateDateFormat = "2006-01-02 15"
const monetaryAuthorityOfSingaporeDataUpdateDateTimezone = "Asia/Singapore"
// MonetaryAuthorityOfSingaporeDataSource defines the structure of exchange rates data source of Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeDataSource struct {
ExchangeRatesDataSource
}
// MonetaryAuthorityOfSingaporeExchangeRateData represents the whole data from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeExchangeRateData struct {
Success bool `json:"success"`
Result *MonetaryAuthorityOfSingaporeResult `json:"result"`
}
// MonetaryAuthorityOfSingaporeResult represents the actual result from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeResult struct {
Records []MonetaryAuthorityOfSingaporeRecord `json:"records"`
}
// MonetaryAuthorityOfSingaporeRecord represents the record from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeRecord map[string]string
// ToLatestExchangeRateResponse returns a view-object according to original data from Monetary Authority of Singapore
func (e *MonetaryAuthorityOfSingaporeExchangeRateData) ToLatestExchangeRateResponse(c *core.Context) *models.LatestExchangeRateResponse {
if !e.Success {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] response is not success")
return nil
}
if e.Result == nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] result is null")
return nil
}
if len(e.Result.Records) < 1 {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] records is empty")
return nil
}
lastDayRecord := e.Result.Records[0]
exchangeRates := make(models.LatestExchangeRateSlice, 0, len(lastDayRecord))
latestUpdateDate := ""
for key, value := range lastDayRecord {
if key == "end_of_day" {
latestUpdateDate = value
continue
}
exchangeRate := e.parseExchangeRateResponse(c, key, value)
if exchangeRate == nil {
continue
}
exchangeRates = append(exchangeRates, exchangeRate)
}
timezone, err := time.LoadLocation(monetaryAuthorityOfSingaporeDataUpdateDateTimezone)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] failed to get timezone, timezone name is %s", monetaryAuthorityOfSingaporeDataUpdateDateTimezone)
return nil
}
updateDateTime := latestUpdateDate + " 12" // These rates are the average of buying and selling interbank rates quoted around midday in Singapore
updateTime, err := time.ParseInLocation(monetaryAuthorityOfSingaporeDataUpdateDateFormat, updateDateTime, timezone)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] failed to parse update date, datetime is %s", updateDateTime)
return nil
}
latestExchangeRateResp := &models.LatestExchangeRateResponse{
DataSource: monetaryAuthorityOfSingaporeDataSource,
ReferenceUrl: monetaryAuthorityOfSingaporeExchangeRateReferenceUrl,
UpdateTime: updateTime.Unix(),
BaseCurrency: monetaryAuthorityOfSingaporeBaseCurrency,
ExchangeRates: exchangeRates,
}
return latestExchangeRateResp
}
func (e *MonetaryAuthorityOfSingaporeExchangeRateData) parseExchangeRateResponse(c *core.Context, key string, value string) *models.LatestExchangeRate {
if !strings.Contains(key, "_") {
return nil
}
items := strings.Split(key, "_")
if len(items) < 2 {
return nil
}
fromCurrencyCode := strings.ToUpper(items[0])
toCurrencyCode := strings.ToUpper(items[1])
if _, exists := validators.AllCurrencyNames[fromCurrencyCode]; !exists {
return nil
}
if toCurrencyCode != monetaryAuthorityOfSingaporeBaseCurrency {
return nil
}
rate, err := utils.StringToFloat64(value)
if err != nil {
log.WarnfWithRequestId(c, "[monetary_authority_of_singapore_datasource.parseExchangeRateResponse] failed to parse rate, rate is %s", value)
return nil
}
if rate <= 0 {
log.WarnfWithRequestId(c, "[monetary_authority_of_singapore_datasource.parseExchangeRateResponse] rate is invalid, rate is %s", value)
return nil
}
finalRate := 1 / rate
if math.IsInf(finalRate, 0) {
return nil
}
if len(items) == 3 && items[2] == "100" {
finalRate = finalRate * 100
}
return &models.LatestExchangeRate{
Currency: fromCurrencyCode,
Rate: utils.Float64ToString(finalRate),
}
}
// GetRequestUrls returns the Monetary Authority of Singapore data source urls
func (e *MonetaryAuthorityOfSingaporeDataSource) GetRequestUrls() []string {
return []string{monetaryAuthorityOfSingaporeExchangeRateUrl}
}
// Parse returns the common response entity according to the Monetary Authority of Singapore data source raw response
func (e *MonetaryAuthorityOfSingaporeDataSource) Parse(c *core.Context, content []byte) (*models.LatestExchangeRateResponse, error) {
monetaryAuthorityOfSingaporeData := &MonetaryAuthorityOfSingaporeExchangeRateData{}
err := json.Unmarshal(content, monetaryAuthorityOfSingaporeData)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.Parse] failed to parse json data, content is %s, because %s", string(content), err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
latestExchangeRateResponse := monetaryAuthorityOfSingaporeData.ToLatestExchangeRateResponse(c)
if latestExchangeRateResponse == nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.Parse] failed to parse latest exchange rate data, content is %s", string(content))
return nil, errs.ErrFailedToRequestRemoteApi
}
return latestExchangeRateResponse, nil
}
@@ -0,0 +1,206 @@
package exchangerates
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/gin-gonic/gin"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const monetaryAuthorityOfSingaporeMinimumRequiredContent = "{\n" +
" \"success\": true,\n" +
" \"result\": {\n" +
" \"records\": [\n" +
" {\n" +
" \"end_of_day\": \"2023-05-26\",\n" +
" \"usd_sgd\": \"1.3528\",\n" +
" \"cny_sgd_100\": \"19.16\"\n" +
" }\n" +
" ]\n" +
" }\n" +
"}"
func TestMonetaryAuthorityOfSingaporeDataSource_StandardDataExtractBaseCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(monetaryAuthorityOfSingaporeMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Equal(t, "SGD", actualLatestExchangeRateResponse.BaseCurrency)
}
func TestMonetaryAuthorityOfSingaporeDataSource_StandardDataExtractExchangeRates(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(monetaryAuthorityOfSingaporeMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "USD",
Rate: "0.7392075694855116",
})
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "CNY",
Rate: "5.219206680584551",
})
}
func TestMonetaryAuthorityOfSingaporeDataSource_BlankContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte(""))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyJsonObject(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_ResponseSuccessIsFalseObject(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": false,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": \"1.3528\",\n"+
" \"cny_sgd_100\": \"19.16\"\n"+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_NoResultContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyRecordContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" ]\n"+
" }\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_TargetCurrencyIsNotBaseCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_cny\": \"1\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_InvalidCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"xxx_sgd\": \"1.3528\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyRate(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": \"\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_InvalidRate(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": null"+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
+24
View File
@@ -0,0 +1,24 @@
package locales
// DefaultLanguage represents the default language
var DefaultLanguage = en
// AllLanguages represents all the supported language
var AllLanguages = map[string]*LocaleInfo{
"en": {
Content: en,
},
"zh-Hans": {
Content: zhHans,
},
}
func GetLocaleTextItems(locale string) *LocaleTextItems {
localeInfo, exists := AllLanguages[locale]
if exists {
return localeInfo.Content
}
return DefaultLanguage
}
+25
View File
@@ -0,0 +1,25 @@
package locales
// LocaleTextItems represents all text items need to be translated
type LocaleTextItems struct {
VerifyEmailTextItems *VerifyEmailTextItems
ForgetPasswordMailTextItems *ForgetPasswordMailTextItems
}
// VerifyEmailTextItems represents text items need to be translated in verify mail
type VerifyEmailTextItems struct {
Title string
SalutationFormat string
DescriptionAboveBtn string
VerifyEmail string
DescriptionBelowBtnFormat string
}
// ForgetPasswordMailTextItems represents text items need to be translated in forget password mail
type ForgetPasswordMailTextItems struct {
Title string
SalutationFormat string
DescriptionAboveBtn string
ResetPassword string
DescriptionBelowBtnFormat string
}
+18
View File
@@ -0,0 +1,18 @@
package locales
var en = &LocaleTextItems{
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "Verify Email",
SalutationFormat: "Hi %s,",
DescriptionAboveBtn: "Please click the link below to confirm your email address.",
VerifyEmail: "Verify Email",
DescriptionBelowBtnFormat: "If you did not sign up for %s account, please simply disregard this email. If you cannot click the link above, please copy the above url and paste it into your browser. The verify email link will be expired after %v minutes.",
},
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
Title: "Reset Your Password",
SalutationFormat: "Hi %s,",
DescriptionAboveBtn: "We recently received a request to reset your password. You can click the link below to reset your password.",
ResetPassword: "Reset Password",
DescriptionBelowBtnFormat: "If you did not request to reset your password, please simply disregard this email. If you cannot click the link above, please copy the above url and paste it into your browser. The password reset link will be expired after %v minutes.",
},
}
+7
View File
@@ -0,0 +1,7 @@
package locales
// LocaleInfo represents locale info
type LocaleInfo struct {
Aliases []string
Content *LocaleTextItems
}
+18
View File
@@ -0,0 +1,18 @@
package locales
var zhHans = &LocaleTextItems{
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "验证邮箱",
SalutationFormat: "%s 您好,",
DescriptionAboveBtn: "请点击下方的链接确认您的邮箱地址。",
VerifyEmail: "验证邮箱",
DescriptionBelowBtnFormat: "如果您没有注册 %s 账户,请直接忽略本邮件。如果您无法点击上述链接,请复制下方的地址然后在您的浏览器中粘贴。邮箱验证链接将在 %v 分钟后过期。",
},
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
Title: "重置密码",
SalutationFormat: "%s 您好,",
DescriptionAboveBtn: "我们刚才收到重置您密码的请求。您可以点击下方链接重置您的密码。",
ResetPassword: "重置密码",
DescriptionBelowBtnFormat: "如果您没有请求重置密码,请直接忽略本邮件。如果您无法点击上述链接,请复制下方的地址然后在您的浏览器中粘贴。重置密码链接将在 %v 分钟后过期。",
},
}
+3 -3
View File
@@ -41,12 +41,12 @@ func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
b.WriteString("] ")
}
b.WriteString(entry.Message)
if requestId, exists := entry.Data[logFieldRequestId]; exists {
b.WriteString(fmt.Sprintf(", r=%s", requestId))
b.WriteString(fmt.Sprintf("[r=%s] ", requestId))
}
b.WriteString(entry.Message)
b.WriteString("\n")
if extra, exists := entry.Data[logFieldExtra]; exists {
+38 -33
View File
@@ -39,11 +39,13 @@ func init() {
}
// SetLoggerConfiguration sets the logger according to the config
func SetLoggerConfiguration(config *settings.Config) error {
func SetLoggerConfiguration(config *settings.Config, isDisableBootLog bool) error {
var bootWriters []io.Writer
var writers []io.Writer
bootWriters = append(bootWriters, os.Stdout)
if !isDisableBootLog {
bootWriters = append(bootWriters, os.Stdout)
}
if config.EnableConsoleLog {
writers = append(writers, os.Stdout)
@@ -56,7 +58,10 @@ func SetLoggerConfiguration(config *settings.Config) error {
return err
}
bootWriters = append(bootWriters, logFile)
if !isDisableBootLog {
bootWriters = append(bootWriters, logFile)
}
writers = append(writers, logFile)
}
@@ -94,93 +99,93 @@ func SetLoggerConfiguration(config *settings.Config) error {
}
// Debugf logs debug log with custom format
func Debugf(format string, args ...interface{}) {
defaultLogger.Debugf(getFinalLog(format, args...))
func Debugf(format string, args ...any) {
defaultLogger.Debug(getFinalLog(format, args...))
}
// DebugfWithRequestId logs debug log with custom format and request id
func DebugfWithRequestId(c *core.Context, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Debugf(getFinalLog(format, args...))
func DebugfWithRequestId(c *core.Context, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Debug(getFinalLog(format, args...))
}
// Infof logs info log with custom format
func Infof(format string, args ...interface{}) {
defaultLogger.Infof(getFinalLog(format, args...))
func Infof(format string, args ...any) {
defaultLogger.Info(getFinalLog(format, args...))
}
// InfofWithRequestId logs info log with custom format and request id
func InfofWithRequestId(c *core.Context, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Infof(getFinalLog(format, args...))
func InfofWithRequestId(c *core.Context, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Info(getFinalLog(format, args...))
}
// Warnf logs warn log with custom format
func Warnf(format string, args ...interface{}) {
defaultLogger.Warnf(getFinalLog(format, args...))
func Warnf(format string, args ...any) {
defaultLogger.Warn(getFinalLog(format, args...))
}
// WarnfWithRequestId logs warn log with custom format and request id
func WarnfWithRequestId(c *core.Context, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Warnf(getFinalLog(format, args...))
func WarnfWithRequestId(c *core.Context, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Warn(getFinalLog(format, args...))
}
// Errorf logs error log with custom format
func Errorf(format string, args ...interface{}) {
defaultLogger.Errorf(getFinalLog(format, args...))
func Errorf(format string, args ...any) {
defaultLogger.Error(getFinalLog(format, args...))
}
// ErrorfWithRequestId logs error log with custom format and request id
func ErrorfWithRequestId(c *core.Context, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Errorf(getFinalLog(format, args...))
func ErrorfWithRequestId(c *core.Context, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Error(getFinalLog(format, args...))
}
// ErrorfWithRequestIdAndExtra logs error log with custom format and request id and extra info
func ErrorfWithRequestIdAndExtra(c *core.Context, extraString string, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).WithField(logFieldExtra, extraString).Errorf(getFinalLog(format, args...))
func ErrorfWithRequestIdAndExtra(c *core.Context, extraString string, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).WithField(logFieldExtra, extraString).Error(getFinalLog(format, args...))
}
// BootInfof logs boot info log
func BootInfof(format string, args ...interface{}) {
func BootInfof(format string, args ...any) {
if bootLogger != nil {
bootLogger.Infof(getFinalLog(format, args...))
bootLogger.Info(getFinalLog(format, args...))
}
}
// BootWarnf logs boot warn log
func BootWarnf(format string, args ...interface{}) {
func BootWarnf(format string, args ...any) {
if bootLogger != nil {
bootLogger.Warnf(getFinalLog(format, args...))
bootLogger.Warn(getFinalLog(format, args...))
}
}
// BootErrorf logs boot error log
func BootErrorf(format string, args ...interface{}) {
func BootErrorf(format string, args ...any) {
if bootLogger != nil {
bootLogger.Errorf(getFinalLog(format, args...))
bootLogger.Error(getFinalLog(format, args...))
}
}
// Requestf logs http request log with custom format
func Requestf(c *core.Context, format string, args ...interface{}) {
func Requestf(c *core.Context, format string, args ...any) {
if requestLogger != nil {
requestLogger.WithField(logFieldRequestId, c.GetRequestId()).Infof(getFinalLog(format, args...))
requestLogger.WithField(logFieldRequestId, c.GetRequestId()).Info(getFinalLog(format, args...))
}
}
// SqlQuery logs sql query log
func SqlQuery(args ...interface{}) {
func SqlQuery(args ...any) {
if sqlQueryLogger != nil {
sqlQueryLogger.Info(args...)
}
}
// SqlQueryf logs sql query log with custom format
func SqlQueryf(format string, args ...interface{}) {
func SqlQueryf(format string, args ...any) {
if sqlQueryLogger != nil {
sqlQueryLogger.Infof(getFinalLog(format, args...))
sqlQueryLogger.Info(getFinalLog(format, args...))
}
}
func getFinalLog(format string, args ...interface{}) string {
func getFinalLog(format string, args ...any) string {
result := fmt.Sprintf(format, args...)
result = strings.Replace(result, "\n", " ", -1)
+63
View File
@@ -0,0 +1,63 @@
package mail
import (
"crypto/tls"
"net"
"gopkg.in/mail.v2"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// DefaultMailer represents default mailer
type DefaultMailer struct {
dialer *mail.Dialer
fromAddress string
}
// NewDefaultMailer returns a new default mailer
func NewDefaultMailer(smtpConfig *settings.SMTPConfig) (*DefaultMailer, error) {
host, portStr, err := net.SplitHostPort(smtpConfig.SMTPHost)
if err != nil {
return nil, errs.ErrSMTPServerHostInvalid
}
port, err := utils.StringToInt(portStr)
if err != nil {
return nil, errs.ErrSMTPServerHostInvalid
}
dialer := mail.NewDialer(host, port, smtpConfig.SMTPUser, smtpConfig.SMTPPasswd)
dialer.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: smtpConfig.SMTPSkipTLSVerify,
}
mailer := &DefaultMailer{
dialer: dialer,
fromAddress: smtpConfig.FromAddress,
}
return mailer, nil
}
// SendMail sends an email according to argument
func (m *DefaultMailer) SendMail(message *MailMessage) error {
if m.dialer == nil {
return errs.ErrSMTPServerNotEnabled
}
mailMessage := mail.NewMessage()
mailMessage.SetHeader("From", m.fromAddress)
mailMessage.SetHeader("To", message.To)
mailMessage.SetHeader("Subject", message.Subject)
mailMessage.SetBody("text/html", message.Body)
err := m.dialer.DialAndSend(mailMessage)
return err
}
+8
View File
@@ -0,0 +1,8 @@
package mail
// MailMessage represents an email entity
type MailMessage struct {
To string
Subject string
Body string
}
+6
View File
@@ -0,0 +1,6 @@
package mail
// Mailer is email sender interface
type Mailer interface {
SendMail(message *MailMessage) error
}
+37
View File
@@ -0,0 +1,37 @@
package mail
import (
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// MailerContainer contains the current mailer
type MailerContainer struct {
Current Mailer
}
// Initialize a mailer container singleton instance
var (
Container = &MailerContainer{}
)
// InitializeMailer initializes the current mailer according to the config
func InitializeMailer(config *settings.Config) error {
if !config.EnableSMTP {
Container.Current = nil
return nil
}
mailer, err := NewDefaultMailer(config.SMTPConfig)
if err != nil {
return err
}
Container.Current = mailer
return nil
}
// SendMail sends an email according to argument
func (u *MailerContainer) SendMail(message *MailMessage) error {
return u.Current.SendMail(message)
}
@@ -0,0 +1,19 @@
package middlewares
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const tokenCookieParam = "ebk_auth_token"
// AmapApiProxyAuthCookie adds amap api proxy auth cookie to cookies in response
func AmapApiProxyAuthCookie(c *core.Context, config *settings.Config) {
token := c.GetTextualToken()
if token != "" {
c.SetCookie(tokenCookieParam, token, int(config.TokenExpiredTime), "/_AMapService", "", false, true)
} else {
c.SetCookie(tokenCookieParam, "", -1, "/_AMapService", "", false, true)
}
}
+98 -46
View File
@@ -1,7 +1,7 @@
package middlewares
import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -10,51 +10,36 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TokenSourceType represents token source
type TokenSourceType byte
// Token source types
const (
TOKEN_SOURCE_TYPE_HEADER TokenSourceType = 1
TOKEN_SOURCE_TYPE_ARGUMENT TokenSourceType = 2
TOKEN_SOURCE_TYPE_COOKIE TokenSourceType = 3
)
const tokenQueryStringParam = "token"
// JWTAuthorization verifies whether current request is valid by jwt token
// JWTAuthorization verifies whether current request is valid by jwt token in header
func JWTAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
if err != nil {
utils.PrintJsonErrorResult(c, err)
return
}
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token requires 2fa", claims.Id)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenRequire2FA)
return
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token type is invalid", claims.Id)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
c.SetTokenClaims(claims)
c.Next()
jwtAuthorization(c, TOKEN_SOURCE_TYPE_HEADER)
}
// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token
// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token in query string
func JWTAuthorizationByQueryString(c *core.Context) {
token, exists := c.GetQuery(tokenQueryStringParam)
jwtAuthorization(c, TOKEN_SOURCE_TYPE_ARGUMENT)
}
if !exists {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorizationByQueryString] no token provided")
utils.PrintJsonErrorResult(c, errs.ErrUnauthorizedAccess)
return
}
c.Request.Header.Set("Authorization", token)
JWTAuthorization(c)
// JWTAuthorizationByCookie verifies whether current request is valid by jwt token in cookie
func JWTAuthorizationByCookie(c *core.Context) {
jwtAuthorization(c, TOKEN_SOURCE_TYPE_COOKIE)
}
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
func JWTTwoFactorAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
if err != nil {
utils.PrintJsonErrorResult(c, err)
@@ -62,7 +47,7 @@ func JWTTwoFactorAuthorization(c *core.Context) {
}
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%s\" token is not need two factor authorization", claims.Id)
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two factor authorization", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenNotRequire2FA)
return
}
@@ -71,12 +56,74 @@ func JWTTwoFactorAuthorization(c *core.Context) {
c.Next()
}
func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
token, claims, err := services.Tokens.ParseToken(c)
// JWTEmailVerifyAuthorization verifies whether current request is email verification
func JWTEmailVerifyAuthorization(c *core.Context) {
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
if err != nil {
utils.PrintJsonErrorResult(c, errs.ErrEmailVerifyTokenIsInvalidOrExpired)
return
}
if claims.Type != core.USER_TOKEN_TYPE_EMAIL_VERIFY {
log.WarnfWithRequestId(c, "[authorization.JWTEmailVerifyAuthorization] user \"uid:%d\" token is not for email verification", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
return
}
c.SetTokenClaims(claims)
c.Next()
}
// JWTResetPasswordAuthorization verifies whether current request is password reset
func JWTResetPasswordAuthorization(c *core.Context) {
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
if err != nil {
utils.PrintJsonErrorResult(c, errs.ErrPasswordResetTokenIsInvalidOrExpired)
return
}
if claims.Type != core.USER_TOKEN_TYPE_PASSWORD_RESET {
log.WarnfWithRequestId(c, "[authorization.JWTResetPasswordAuthorization] user \"uid:%d\" token is not for password request", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
return
}
c.SetTokenClaims(claims)
c.Next()
}
func jwtAuthorization(c *core.Context, source TokenSourceType) {
claims, err := getTokenClaims(c, source)
if err != nil {
utils.PrintJsonErrorResult(c, err)
return
}
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.jwtAuthorization] user \"uid:%d\" token requires 2fa", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenRequire2FA)
return
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type is invalid", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
c.SetTokenClaims(claims)
c.Next()
}
func getTokenClaims(c *core.Context, source TokenSourceType) (*core.UserTokenClaims, *errs.Error) {
token, claims, err := parseToken(c, source)
if err != nil {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] failed to parse token, because %s", err.Error())
return nil, errs.ErrUnauthorizedAccess
return nil, errs.Or(err, errs.ErrUnauthorizedAccess)
}
if !token.Valid {
@@ -84,15 +131,20 @@ func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
return nil, errs.ErrCurrentInvalidToken
}
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is expired")
return nil, errs.ErrCurrentTokenExpired
}
if claims.Id == "" {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is empty")
if claims.Uid <= 0 {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is invalid")
return nil, errs.ErrCurrentInvalidToken
}
return claims, nil
}
func parseToken(c *core.Context, source TokenSourceType) (*jwt.Token, *core.UserTokenClaims, error) {
if source == TOKEN_SOURCE_TYPE_ARGUMENT {
return services.Tokens.ParseTokenByArgument(c, tokenQueryStringParam)
} else if source == TOKEN_SOURCE_TYPE_COOKIE {
return services.Tokens.ParseTokenByCookie(c, tokenCookieParam)
}
return services.Tokens.ParseTokenByHeader(c)
}
-16
View File
@@ -1,16 +0,0 @@
package middlewares
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
const utcOffsetQueryStringParam = "utc_offset"
// HeaderInQueryString puts some headers from query string
func HeaderInQueryString(c *core.Context) {
utcOffset, exists := c.GetQuery(utcOffsetQueryStringParam)
if exists {
c.Request.Header.Set(core.ClientTimezoneOffsetHeaderName, utcOffset)
}
}
+2 -2
View File
@@ -3,7 +3,7 @@ package middlewares
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"runtime"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -49,7 +49,7 @@ func stack(skip int) []byte {
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile {
data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
continue
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// RequestLog logs the http request log
@@ -18,7 +19,7 @@ func RequestLog(c *core.Context) {
now := time.Now()
statusCode := c.Writer.Status()
errorCode := 0
errorCode := int32(0)
userId := "-"
claims := c.GetTokenClaims()
@@ -28,7 +29,7 @@ func RequestLog(c *core.Context) {
method := c.Request.Method
if claims != nil {
userId = claims.Id
userId = utils.Int64ToString(claims.Uid)
}
if err != nil {
+54 -1
View File
@@ -1,7 +1,9 @@
package middlewares
import (
"encoding/base64"
"fmt"
"net/url"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -15,16 +17,67 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
return func(c *core.Context) {
settingsArr := []string{
buildBooleanSetting("r", config.EnableUserRegister),
buildBooleanSetting("f", config.EnableUserForgetPassword),
buildBooleanSetting("v", config.EnableUserVerifyEmail),
buildBooleanSetting("e", config.EnableDataExport),
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
}
if config.EnableMapDataFetchProxy &&
(config.MapProvider == settings.OpenStreetMapProvider ||
config.MapProvider == settings.OpenStreetMapHumanitarianStyleProvider ||
config.MapProvider == settings.OpenTopoMapProvider ||
config.MapProvider == settings.OPNVKarteMapProvider ||
config.MapProvider == settings.CyclOSMMapProvider ||
config.MapProvider == settings.TomTomMapProvider) {
settingsArr = append(settingsArr, buildBooleanSetting("mp", config.EnableMapDataFetchProxy))
}
if config.MapProvider == settings.TomTomMapProvider && config.TomTomMapAPIKey != "" && !config.EnableMapDataFetchProxy {
settingsArr = append(settingsArr, buildEncodedStringSetting("tmak", config.TomTomMapAPIKey))
}
if config.MapProvider == settings.GoogleMapProvider && config.GoogleMapAPIKey != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("gmak", config.GoogleMapAPIKey))
}
if config.MapProvider == settings.BaiduMapProvider && config.BaiduMapAK != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("bmak", config.BaiduMapAK))
}
if config.MapProvider == settings.AmapProvider && config.AmapApplicationKey != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("amak", config.AmapApplicationKey))
}
if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod != "" {
settingsArr = append(settingsArr, buildStringSetting("amsv", strings.Replace(config.AmapSecurityVerificationMethod, "_", "", -1)))
if config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationExternalProxyMethod {
settingsArr = append(settingsArr, buildEncodedStringSetting("amep", config.AmapApiExternalProxyUrl))
}
if config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationPlainTextMethod {
settingsArr = append(settingsArr, buildEncodedStringSetting("amas", config.AmapApplicationSecret))
}
}
bundledSettings := strings.Join(settingsArr, "_")
c.SetCookie(settingsCookieName, bundledSettings, config.TokenExpiredTime, "", "", false, false)
c.SetCookie(settingsCookieName, bundledSettings, int(config.TokenExpiredTime), "", "", false, false)
c.Next()
}
}
func buildStringSetting(key string, value string) string {
return fmt.Sprintf("%s.%s", key, value)
}
func buildEncodedStringSetting(key string, value string) string {
urlEncodedValue := url.QueryEscape(value)
base64Value := base64.StdEncoding.EncodeToString([]byte(urlEncodedValue))
return fmt.Sprintf("%s.%s", key, base64Value)
}
func buildBooleanSetting(key string, value bool) string {
if value {
return fmt.Sprintf("%s.1", key)
+3 -3
View File
@@ -55,7 +55,7 @@ type Account struct {
Type AccountType `xorm:"NOT NULL"`
ParentAccountId int64 `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
Name string `xorm:"VARCHAR(32) NOT NULL"`
DisplayOrder int `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
DisplayOrder int32 `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
Icon int64 `xorm:"NOT NULL"`
Color string `xorm:"VARCHAR(6) NOT NULL"`
Currency string `xorm:"VARCHAR(3) NOT NULL"`
@@ -116,7 +116,7 @@ type AccountMoveRequest struct {
// AccountNewDisplayOrderRequest represents a data pair of id and display order
type AccountNewDisplayOrderRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
DisplayOrder int `json:"displayOrder"`
DisplayOrder int32 `json:"displayOrder"`
}
// AccountDeleteRequest represents all parameters of account deleting request
@@ -136,7 +136,7 @@ type AccountInfoResponse struct {
Currency string `json:"currency"`
Balance int64 `json:"balance"`
Comment string `json:"comment"`
DisplayOrder int `json:"displayOrder"`
DisplayOrder int32 `json:"displayOrder"`
IsAsset bool `json:"isAsset,omitempty"`
IsLiability bool `json:"isLiability,omitempty"`
Hidden bool `json:"hidden"`
+7
View File
@@ -6,3 +6,10 @@ type AuthResponse struct {
Need2FA bool `json:"need2FA"`
User *UserBasicInfo `json:"user"`
}
// RegisterResponse returns a view-object of user register response
type RegisterResponse struct {
AuthResponse
NeedVerifyEmail bool `json:"needVerifyEmail"`
PresetCategoriesSaved bool `json:"presetCategoriesSaved"`
}
+8
View File
@@ -4,3 +4,11 @@ package models
type ClearDataRequest struct {
Password string `json:"password" binding:"omitempty,min=6,max=128"`
}
// DataStatisticsResponse represents a view-object of user data statistic
type DataStatisticsResponse struct {
TotalAccountCount int64 `json:"totalAccountCount,string"`
TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"`
TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"`
TotalTransactionCount int64 `json:"totalTransactionCount,string"`
}
+162
View File
@@ -0,0 +1,162 @@
package models
import "fmt"
// WeekDay represents week day
type WeekDay byte
// Week days
const (
WEEKDAY_SUNDAY WeekDay = 0
WEEKDAY_MONDAY WeekDay = 1
WEEKDAY_TUESDAY WeekDay = 2
WEEKDAY_WEDNESDAY WeekDay = 3
WEEKDAY_THURSDAY WeekDay = 4
WEEKDAY_FRIDAY WeekDay = 5
WEEKDAY_SATURDAY WeekDay = 6
WEEKDAY_INVALID WeekDay = 255
)
// String returns a textual representation of the week day enum
func (d WeekDay) String() string {
switch d {
case WEEKDAY_SUNDAY:
return "Sunday"
case WEEKDAY_MONDAY:
return "Monday"
case WEEKDAY_TUESDAY:
return "Tuesday"
case WEEKDAY_WEDNESDAY:
return "Wednesday"
case WEEKDAY_THURSDAY:
return "Thursday"
case WEEKDAY_FRIDAY:
return "Friday"
case WEEKDAY_SATURDAY:
return "Saturday"
case WEEKDAY_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
// LongDateFormat represents long date format
type LongDateFormat byte
// Long Date Format
const (
LONG_DATE_FORMAT_DEFAULT LongDateFormat = 0
LONG_DATE_FORMAT_YYYY_M_D LongDateFormat = 1
LONG_DATE_FORMAT_M_D_YYYY LongDateFormat = 2
LONG_DATE_FORMAT_D_M_YYYY LongDateFormat = 3
LONG_DATE_FORMAT_INVALID LongDateFormat = 255
)
// String returns a textual representation of the long date format enum
func (f LongDateFormat) String() string {
switch f {
case LONG_DATE_FORMAT_DEFAULT:
return "Default"
case LONG_DATE_FORMAT_YYYY_M_D:
return "YYYY_MM_D"
case LONG_DATE_FORMAT_M_D_YYYY:
return "M_D_YYYY"
case LONG_DATE_FORMAT_D_M_YYYY:
return "D_M_YYYY"
case LONG_DATE_FORMAT_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// ShortDateFormat represents short date format
type ShortDateFormat byte
// Short Date Format
const (
SHORT_DATE_FORMAT_DEFAULT ShortDateFormat = 0
SHORT_DATE_FORMAT_YYYY_M_D ShortDateFormat = 1
SHORT_DATE_FORMAT_M_D_YYYY ShortDateFormat = 2
SHORT_DATE_FORMAT_D_M_YYYY ShortDateFormat = 3
SHORT_DATE_FORMAT_INVALID ShortDateFormat = 255
)
// String returns a textual representation of the short date format enum
func (f ShortDateFormat) String() string {
switch f {
case SHORT_DATE_FORMAT_DEFAULT:
return "Default"
case SHORT_DATE_FORMAT_YYYY_M_D:
return "YYYY_MM_D"
case SHORT_DATE_FORMAT_M_D_YYYY:
return "M_D_YYYY"
case SHORT_DATE_FORMAT_D_M_YYYY:
return "D_M_YYYY"
case SHORT_DATE_FORMAT_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// LongTimeFormat represents long time format
type LongTimeFormat byte
// Long Time Format
const (
LONG_TIME_FORMAT_DEFAULT LongTimeFormat = 0
LONG_TIME_FORMAT_HH_MM_SS LongTimeFormat = 1
LONG_TIME_FORMAT_A_HH_MM_SS LongTimeFormat = 2
LONG_TIME_FORMAT_HH_MM_SS_A LongTimeFormat = 3
LONG_TIME_FORMAT_INVALID LongTimeFormat = 255
)
// String returns a textual representation of the long time format enum
func (f LongTimeFormat) String() string {
switch f {
case LONG_TIME_FORMAT_DEFAULT:
return "Default"
case LONG_TIME_FORMAT_HH_MM_SS:
return "HH_MM_SS"
case LONG_TIME_FORMAT_A_HH_MM_SS:
return "A_HH_MM_SS"
case LONG_TIME_FORMAT_HH_MM_SS_A:
return "HH_MM_SS_A"
case LONG_TIME_FORMAT_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// ShortTimeFormat represents short time format
type ShortTimeFormat byte
// Short Time Format
const (
SHORT_TIME_FORMAT_DEFAULT ShortTimeFormat = 0
SHORT_TIME_FORMAT_HH_MM ShortTimeFormat = 1
SHORT_TIME_FORMAT_A_HH_MM ShortTimeFormat = 2
SHORT_TIME_FORMAT_HH_MM_A ShortTimeFormat = 3
SHORT_TIME_FORMAT_INVALID ShortTimeFormat = 255
)
// String returns a textual representation of the short time format enum
func (f ShortTimeFormat) String() string {
switch f {
case SHORT_TIME_FORMAT_DEFAULT:
return "Default"
case SHORT_TIME_FORMAT_HH_MM:
return "HH_MM"
case SHORT_TIME_FORMAT_A_HH_MM:
return "A_HH_MM"
case SHORT_TIME_FORMAT_HH_MM_A:
return "HH_MM_A"
case SHORT_TIME_FORMAT_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
+12
View File
@@ -0,0 +1,12 @@
package models
// ForgetPasswordRequest represents all parameters of forget password request
type ForgetPasswordRequest struct {
Email string `json:"email" binding:"required,notBlank,max=100,validEmail"`
}
// PasswordResetRequest represents all parameters of reset password request
type PasswordResetRequest struct {
Email string `json:"email" binding:"required,notBlank,max=100,validEmail"`
Password string `json:"password" binding:"required,min=6,max=128"`
}
+78 -31
View File
@@ -35,8 +35,8 @@ const (
// Transaction represents transaction data stored in database
type Transaction struct {
TransactionId int64 `xorm:"PK"`
Uid int64 `xorm:"UNIQUE(UQE_transaction_uid_time) INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) NOT NULL"`
Uid int64 `xorm:"UNIQUE(UQE_transaction_uid_time) INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) INDEX(IDX_transaction_uid_deleted_time_longitude_latitude) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) INDEX(IDX_transaction_uid_deleted_time_longitude_latitude) NOT NULL"`
Type TransactionDbType `xorm:"INDEX(IDX_transaction_uid_deleted_type_time) NOT NULL"`
CategoryId int64 `xorm:"INDEX(IDX_transaction_uid_deleted_category_id_time) NOT NULL"`
AccountId int64 `xorm:"INDEX(IDX_transaction_uid_deleted_account_id_time) NOT NULL"`
@@ -48,39 +48,50 @@ type Transaction struct {
RelatedAccountAmount int64 `xorm:"NOT NULL"`
HideAmount bool `xorm:"NOT NULL"`
Comment string `xorm:"VARCHAR(255) NOT NULL"`
GeoLongitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
GeoLatitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
CreatedIp string `xorm:"VARCHAR(39)"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
}
// TransactionGeoLocationRequest represents all parameters of transaction geographic location info update request
type TransactionGeoLocationRequest struct {
Latitude float64 `json:"latitude" binding:"required"`
Longitude float64 `json:"longitude" binding:"required"`
}
// TransactionCreateRequest represents all parameters of transaction creation request
type TransactionCreateRequest struct {
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
}
// TransactionModifyRequest represents all parameters of transaction modification request
type TransactionModifyRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
Id int64 `json:"id,string" binding:"required,min=1"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
}
// TransactionCountRequest represents transaction count request
@@ -101,7 +112,9 @@ type TransactionListByMaxTimeRequest struct {
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
Count int `form:"count" binding:"required,min=1,max=50"`
Page int32 `form:"page" binding:"min=0"`
Count int32 `form:"count" binding:"required,min=1,max=50"`
WithCount bool `form:"with_count"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
@@ -109,14 +122,12 @@ type TransactionListByMaxTimeRequest struct {
// TransactionListInMonthByPageRequest represents all parameters of transaction listing by month request
type TransactionListInMonthByPageRequest struct {
Year int `form:"year" binding:"required,min=1"`
Month int `form:"month" binding:"required,min=1"`
Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"`
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
Page int `form:"page" binding:"required,min=1"`
Count int `form:"count" binding:"required,min=1,max=50"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
@@ -169,6 +180,12 @@ type TransactionAccountAmount struct {
TotalExpenseAmount int64
}
// TransactionGeoLocationResponse represents a view-object of transaction geographic location info
type TransactionGeoLocationResponse struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
// TransactionInfoResponse represents a view-object of transaction
type TransactionInfoResponse struct {
Id int64 `json:"id,string"`
@@ -188,6 +205,7 @@ type TransactionInfoResponse struct {
TagIds []string `json:"tagIds"`
Tags []*TransactionTagInfoResponse `json:"tags,omitempty"`
Comment string `json:"comment"`
GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"`
Editable bool `json:"editable"`
}
@@ -200,6 +218,7 @@ type TransactionCountResponse struct {
type TransactionInfoPageWrapperResponse struct {
Items TransactionInfoResponseSlice `json:"items"`
NextTimeSequenceId *int64 `json:"nextTimeSequenceId,string"`
TotalCount *int64 `json:"totalCount,omitempty"`
}
// TransactionInfoPageWrapperResponse2 represents a response of transaction which contains items and count
@@ -231,8 +250,8 @@ type TransactionAmountsResponseItem struct {
// TransactionMonthAmountsResponseItem represents an item of transaction month amounts
type TransactionMonthAmountsResponseItem struct {
Year int `json:"year"`
Month int `json:"month"`
Year int32 `json:"year"`
Month int32 `json:"month"`
Amounts []*TransactionAmountsResponseItemAmountInfo `json:"amounts"`
}
@@ -297,6 +316,15 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *
destinationAmount = t.Amount
}
geoLocation := &TransactionGeoLocationResponse{}
if t.GeoLongitude != 0 || t.GeoLatitude != 0 {
geoLocation.Longitude = t.GeoLongitude
geoLocation.Latitude = t.GeoLatitude
} else {
geoLocation = nil
}
return &TransactionInfoResponse{
Id: t.TransactionId,
TimeSequenceId: t.TransactionTime,
@@ -311,6 +339,7 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *
HideAmount: t.HideAmount,
TagIds: utils.Int64ArrayToStringArray(tagIds),
Comment: t.Comment,
GeoLocation: geoLocation,
Editable: editable,
}
}
@@ -423,3 +452,21 @@ func (s TransactionMonthAmountsResponseItemSlice) Less(i, j int) bool {
return s[i].Month > s[j].Month
}
// TransactionAmountsResponseItemAmountInfoSlice represents the slice data structure of TransactionAmountsResponseItemAmountInfo
type TransactionAmountsResponseItemAmountInfoSlice []*TransactionAmountsResponseItemAmountInfo
// Len returns the count of items
func (s TransactionAmountsResponseItemAmountInfoSlice) Len() int {
return len(s)
}
// Swap swaps two items
func (s TransactionAmountsResponseItemAmountInfoSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less reports whether the first item is less than the second one
func (s TransactionAmountsResponseItemAmountInfoSlice) Less(i, j int) bool {
return strings.Compare(s[i].Currency, s[j].Currency) < 0
}
+3 -3
View File
@@ -21,7 +21,7 @@ type TransactionCategory struct {
Type TransactionCategoryType `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
ParentCategoryId int64 `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
Name string `xorm:"VARCHAR(32) NOT NULL"`
DisplayOrder int `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
DisplayOrder int32 `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
Icon int64 `xorm:"NOT NULL"`
Color string `xorm:"VARCHAR(6) NOT NULL"`
Hidden bool `xorm:"NOT NULL"`
@@ -91,7 +91,7 @@ type TransactionCategoryMoveRequest struct {
// TransactionCategoryNewDisplayOrderRequest represents a data pair of id and display order
type TransactionCategoryNewDisplayOrderRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
DisplayOrder int `json:"displayOrder"`
DisplayOrder int32 `json:"displayOrder"`
}
// TransactionCategoryDeleteRequest represents all parameters of transaction category deleting request
@@ -108,7 +108,7 @@ type TransactionCategoryInfoResponse struct {
Icon int64 `json:"icon,string"`
Color string `json:"color"`
Comment string `json:"comment"`
DisplayOrder int `json:"displayOrder"`
DisplayOrder int32 `json:"displayOrder"`
Hidden bool `json:"hidden"`
SubCategories TransactionCategoryInfoResponseSlice `json:"subCategories,omitempty"`
}
+3 -3
View File
@@ -6,7 +6,7 @@ type TransactionTag struct {
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_name) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_name) NOT NULL"`
Name string `xorm:"INDEX(IDX_tag_uid_deleted_name) VARCHAR(32) NOT NULL"`
DisplayOrder int `xorm:"NOT NULL"`
DisplayOrder int32 `xorm:"NOT NULL"`
Hidden bool `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
@@ -43,7 +43,7 @@ type TransactionTagMoveRequest struct {
// TransactionTagNewDisplayOrderRequest represents a data pair of id and display order
type TransactionTagNewDisplayOrderRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
DisplayOrder int `json:"displayOrder"`
DisplayOrder int32 `json:"displayOrder"`
}
// TransactionTagDeleteRequest represents all parameters of transaction tag deleting request
@@ -55,7 +55,7 @@ type TransactionTagDeleteRequest struct {
type TransactionTagInfoResponse struct {
Id int64 `json:"id,string"`
Name string `json:"name"`
DisplayOrder int `json:"displayOrder"`
DisplayOrder int32 `json:"displayOrder"`
Hidden bool `json:"hidden"`
}
+95 -51
View File
@@ -4,48 +4,10 @@ import (
"fmt"
"time"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// WeekDay represents week day
type WeekDay byte
// Week days
const (
WEEKDAY_SUNDAY WeekDay = 0
WEEKDAY_MONDAY WeekDay = 1
WEEKDAY_TUESDAY WeekDay = 2
WEEKDAY_WEDNESDAY WeekDay = 3
WEEKDAY_THURSDAY WeekDay = 4
WEEKDAY_FRIDAY WeekDay = 5
WEEKDAY_SATURDAY WeekDay = 6
WEEKDAY_INVALID WeekDay = 255
)
// String returns a textual representation of the week day enum
func (d WeekDay) String() string {
switch d {
case WEEKDAY_SUNDAY:
return "Sunday"
case WEEKDAY_MONDAY:
return "Monday"
case WEEKDAY_TUESDAY:
return "Tuesday"
case WEEKDAY_WEDNESDAY:
return "Wednesday"
case WEEKDAY_THURSDAY:
return "Thursday"
case WEEKDAY_FRIDAY:
return "Friday"
case WEEKDAY_SATURDAY:
return "Saturday"
case WEEKDAY_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
// TransactionEditScope represents the scope which transaction can be edited
type TransactionEditScope byte
@@ -87,15 +49,22 @@ func (s TransactionEditScope) String() string {
// User represents user data stored in database
type User struct {
Uid int64 `xorm:"PK"`
Username string `xorm:"VARCHAR(32) UNIQUE NOT NULL"`
Email string `xorm:"VARCHAR(100) UNIQUE NOT NULL"`
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
Password string `xorm:"VARCHAR(64) NOT NULL"`
Salt string `xorm:"VARCHAR(10) NOT NULL"`
Uid int64 `xorm:"PK"`
Username string `xorm:"VARCHAR(32) UNIQUE NOT NULL"`
Email string `xorm:"VARCHAR(100) UNIQUE NOT NULL"`
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
Password string `xorm:"VARCHAR(64) NOT NULL"`
Salt string `xorm:"VARCHAR(10) NOT NULL"`
DefaultAccountId int64
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
Language string `xorm:"VARCHAR(10)"`
DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"`
FirstDayOfWeek WeekDay `xorm:"TINYINT NOT NULL"`
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
LongDateFormat LongDateFormat `xorm:"TINYINT"`
ShortDateFormat ShortDateFormat `xorm:"TINYINT"`
LongTimeFormat LongTimeFormat `xorm:"TINYINT"`
ShortTimeFormat ShortTimeFormat `xorm:"TINYINT"`
Disabled bool `xorm:"NOT NULL"`
Deleted bool `xorm:"NOT NULL"`
EmailVerified bool `xorm:"NOT NULL"`
CreatedUnixTime int64
@@ -109,9 +78,18 @@ type UserBasicInfo struct {
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar"`
AvatarProvider string `json:"avatarProvider,omitempty"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
EmailVerified bool `json:"emailVerified"`
}
// UserLoginRequest represents all parameters of user login request
@@ -126,8 +104,27 @@ type UserRegisterRequest struct {
Email string `json:"email" binding:"required,notBlank,max=100,validEmail"`
Nickname string `json:"nickname" binding:"required,notBlank,max=64"`
Password string `json:"password" binding:"required,min=6,max=128"`
Language string `json:"language" binding:"required,min=2,max=16"`
DefaultCurrency string `json:"defaultCurrency" binding:"required,len=3,validCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek" binding:"min=0,max=6"`
TransactionCategoryCreateBatchRequest
}
// UserVerifyEmailRequest represents all parameters of user verify email request
type UserVerifyEmailRequest struct {
RequestNewToken bool `json:"requestNewToken" binding:"omitempty"`
}
// UserVerifyEmailResponse represents all response parameters after user have verified email
type UserVerifyEmailResponse struct {
NewToken string `json:"newToken,omitempty"`
User *UserBasicInfo `json:"user"`
}
// UserResendVerifyEmailRequest represents all parameters of user resend verify email request
type UserResendVerifyEmailRequest struct {
Email string `json:"email" binding:"omitempty,max=100,validEmail"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
}
// UserProfileUpdateRequest represents all parameters of user updating profile request
@@ -136,9 +133,15 @@ type UserProfileUpdateRequest struct {
Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
OldPassword string `json:"oldPassword" binding:"omitempty,min=6,max=128"`
DefaultAccountId int64 `json:"defaultAccountId,string" binding:"omitempty,min=1"`
TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=7"`
Language string `json:"language" binding:"omitempty,min=2,max=16"`
DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"`
FirstDayOfWeek *WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"`
TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=7"`
LongDateFormat *LongDateFormat `json:"longDateFormat" binding:"omitempty,min=0,max=3"`
ShortDateFormat *ShortDateFormat `json:"shortDateFormat" binding:"omitempty,min=0,max=3"`
LongTimeFormat *LongTimeFormat `json:"longTimeFormat" binding:"omitempty,min=0,max=3"`
ShortTimeFormat *ShortTimeFormat `json:"shortTimeFormat" binding:"omitempty,min=0,max=3"`
}
// UserProfileUpdateResponse represents the data returns to frontend after updating profile
@@ -152,9 +155,18 @@ type UserProfileResponse struct {
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar"`
AvatarProvider string `json:"avatarProvider,omitempty"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
EmailVerified bool `json:"emailVerified"`
LastLoginAt int64 `json:"lastLoginAt"`
}
@@ -206,9 +218,18 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo {
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
AvatarUrl: u.getAvatarUrl(),
AvatarProvider: u.getAvatarProvider(),
DefaultAccountId: u.DefaultAccountId,
TransactionEditScope: u.TransactionEditScope,
Language: u.Language,
DefaultCurrency: u.DefaultCurrency,
FirstDayOfWeek: u.FirstDayOfWeek,
TransactionEditScope: u.TransactionEditScope,
LongDateFormat: u.LongDateFormat,
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
EmailVerified: u.EmailVerified,
}
}
@@ -218,9 +239,32 @@ func (u *User) ToUserProfileResponse() *UserProfileResponse {
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
AvatarUrl: u.getAvatarUrl(),
AvatarProvider: u.getAvatarProvider(),
DefaultAccountId: u.DefaultAccountId,
TransactionEditScope: u.TransactionEditScope,
Language: u.Language,
DefaultCurrency: u.DefaultCurrency,
FirstDayOfWeek: u.FirstDayOfWeek,
TransactionEditScope: u.TransactionEditScope,
LongDateFormat: u.LongDateFormat,
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
EmailVerified: u.EmailVerified,
LastLoginAt: u.LastLoginUnixTime,
}
}
func (u *User) getAvatarProvider() string {
return settings.Container.Current.AvatarProvider
}
func (u *User) getAvatarUrl() string {
avatarProvider := settings.Container.Current.AvatarProvider
if avatarProvider == settings.GravatarProvider {
return utils.GetGravatarUrl(u.Email)
}
return ""
}
@@ -45,7 +45,7 @@ type RequestIdInfo struct {
type DefaultRequestIdGenerator struct {
serverUniqId uint16
instanceUniqId uint16
requestSeqId uint32
requestSeqId atomic.Uint32
}
// NewDefaultRequestIdGenerator returns a new default request id generator
@@ -154,7 +154,7 @@ func (r *DefaultRequestIdGenerator) getRequestId(serverUniqId uint16, instanceUn
secondsAndRandomNumber := (secondsLow17bits << randomNumberBits) | randomNumberLow15bits
seqId := atomic.AddUint32(&r.requestSeqId, 1)
seqId := r.requestSeqId.Add(1)
seqIdLow31bits := seqId & reqSeqNumberBitsMask
seqIdAndClientIpv6Flag := (seqIdLow31bits << clientIpv6Bit) | (clientIpv6Flag & clientIpv6BitMask)
+66 -21
View File
@@ -5,6 +5,7 @@ import (
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -30,20 +31,31 @@ var (
}
)
// GetTotalAccountCountByUid returns total account count of user
func (s *AccountService) GetTotalAccountCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Count(&models.Account{})
return count, err
}
// GetAllAccountsByUid returns all account models of user
func (s *AccountService) GetAllAccountsByUid(uid int64) ([]*models.Account, error) {
func (s *AccountService) GetAllAccountsByUid(c *core.Context, uid int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var accounts []*models.Account
err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).OrderBy("parent_account_id asc, display_order asc").Find(&accounts)
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).OrderBy("parent_account_id asc, display_order asc").Find(&accounts)
return accounts, err
}
// GetAccountAndSubAccountsByAccountId returns account model and sub account models according to account id
func (s *AccountService) GetAccountAndSubAccountsByAccountId(uid int64, accountId int64) ([]*models.Account, error) {
func (s *AccountService) GetAccountAndSubAccountsByAccountId(c *core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -53,13 +65,29 @@ func (s *AccountService) GetAccountAndSubAccountsByAccountId(uid int64, accountI
}
var accounts []*models.Account
err := s.UserDataDB(uid).Where("uid=? AND deleted=? AND (account_id=? OR parent_account_id=?)", uid, false, accountId, accountId).OrderBy("parent_account_id asc, display_order asc").Find(&accounts)
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND (account_id=? OR parent_account_id=?)", uid, false, accountId, accountId).OrderBy("parent_account_id asc, display_order asc").Find(&accounts)
return accounts, err
}
// GetSubAccountsByAccountId returns sub account models according to account id
func (s *AccountService) GetSubAccountsByAccountId(c *core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if accountId <= 0 {
return nil, errs.ErrAccountIdInvalid
}
var accounts []*models.Account
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND parent_account_id=?", uid, false, accountId).OrderBy("display_order asc").Find(&accounts)
return accounts, err
}
// GetAccountsByAccountIds returns account models according to account ids
func (s *AccountService) GetAccountsByAccountIds(uid int64, accountIds []int64) (map[int64]*models.Account, error) {
func (s *AccountService) GetAccountsByAccountIds(c *core.Context, uid int64, accountIds []int64) (map[int64]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -69,7 +97,7 @@ func (s *AccountService) GetAccountsByAccountIds(uid int64, accountIds []int64)
}
var accounts []*models.Account
err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).In("account_id", accountIds).Find(&accounts)
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("account_id", accountIds).Find(&accounts)
if err != nil {
return nil, err
@@ -80,13 +108,13 @@ func (s *AccountService) GetAccountsByAccountIds(uid int64, accountIds []int64)
}
// GetMaxDisplayOrder returns the max display order according to account category
func (s *AccountService) GetMaxDisplayOrder(uid int64, category models.AccountCategory) (int, error) {
func (s *AccountService) GetMaxDisplayOrder(c *core.Context, uid int64, category models.AccountCategory) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
account := &models.Account{}
has, err := s.UserDataDB(uid).Cols("uid", "deleted", "parent_account_id", "display_order").Where("uid=? AND deleted=? AND parent_account_id=? AND category=?", uid, false, models.LevelOneAccountParentId, category).OrderBy("display_order desc").Limit(1).Get(account)
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "parent_account_id", "display_order").Where("uid=? AND deleted=? AND parent_account_id=? AND category=?", uid, false, models.LevelOneAccountParentId, category).OrderBy("display_order desc").Limit(1).Get(account)
if err != nil {
return 0, err
@@ -100,7 +128,7 @@ func (s *AccountService) GetMaxDisplayOrder(uid int64, category models.AccountCa
}
// GetMaxSubAccountDisplayOrder returns the max display order of sub account according to account category and parent account id
func (s *AccountService) GetMaxSubAccountDisplayOrder(uid int64, category models.AccountCategory, parentAccountId int64) (int, error) {
func (s *AccountService) GetMaxSubAccountDisplayOrder(c *core.Context, uid int64, category models.AccountCategory, parentAccountId int64) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
@@ -110,7 +138,7 @@ func (s *AccountService) GetMaxSubAccountDisplayOrder(uid int64, category models
}
account := &models.Account{}
has, err := s.UserDataDB(uid).Cols("uid", "deleted", "parent_account_id", "display_order").Where("uid=? AND deleted=? AND parent_account_id=? AND category=?", uid, false, parentAccountId, category).OrderBy("display_order desc").Limit(1).Get(account)
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "parent_account_id", "display_order").Where("uid=? AND deleted=? AND parent_account_id=? AND category=?", uid, false, parentAccountId, category).OrderBy("display_order desc").Limit(1).Get(account)
if err != nil {
return 0, err
@@ -124,7 +152,7 @@ func (s *AccountService) GetMaxSubAccountDisplayOrder(uid int64, category models
}
// CreateAccounts saves a new account model to database
func (s *AccountService) CreateAccounts(mainAccount *models.Account, childrenAccounts []*models.Account) error {
func (s *AccountService) CreateAccounts(c *core.Context, mainAccount *models.Account, childrenAccounts []*models.Account, utcOffset int16) error {
if mainAccount.Uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -135,12 +163,22 @@ func (s *AccountService) CreateAccounts(mainAccount *models.Account, childrenAcc
var allInitTransactions []*models.Transaction
mainAccount.AccountId = s.GenerateUuid(uuid.UUID_TYPE_ACCOUNT)
if mainAccount.AccountId < 1 {
return errs.ErrSystemIsBusy
}
allAccounts[0] = mainAccount
if mainAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
for i := 0; i < len(childrenAccounts); i++ {
childAccount := childrenAccounts[i]
childAccount.AccountId = s.GenerateUuid(uuid.UUID_TYPE_ACCOUNT)
if childAccount.AccountId < 1 {
return errs.ErrSystemIsBusy
}
childAccount.ParentAccountId = mainAccount.AccountId
childAccount.Uid = mainAccount.Uid
childAccount.Type = models.ACCOUNT_TYPE_SINGLE_ACCOUNT
@@ -157,12 +195,19 @@ func (s *AccountService) CreateAccounts(mainAccount *models.Account, childrenAcc
allAccounts[i].UpdatedUnixTime = now
if allAccounts[i].Balance != 0 {
transactionId := s.GenerateUuid(uuid.UUID_TYPE_TRANSACTION)
if transactionId < 1 {
return errs.ErrSystemIsBusy
}
newTransaction := &models.Transaction{
TransactionId: s.GenerateUuid(uuid.UUID_TYPE_TRANSACTION),
TransactionId: transactionId,
Uid: allAccounts[i].Uid,
Deleted: false,
Type: models.TRANSACTION_DB_TYPE_MODIFY_BALANCE,
TransactionTime: transactionTime,
TimezoneUtcOffset: utcOffset,
AccountId: allAccounts[i].AccountId,
Amount: allAccounts[i].Balance,
RelatedAccountId: allAccounts[i].AccountId,
@@ -176,7 +221,7 @@ func (s *AccountService) CreateAccounts(mainAccount *models.Account, childrenAcc
}
}
return s.UserDataDB(mainAccount.Uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(mainAccount.Uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(allAccounts); i++ {
account := allAccounts[i]
_, err := sess.Insert(account)
@@ -200,7 +245,7 @@ func (s *AccountService) CreateAccounts(mainAccount *models.Account, childrenAcc
}
// ModifyAccounts saves an existed account model to database
func (s *AccountService) ModifyAccounts(uid int64, accounts []*models.Account) error {
func (s *AccountService) ModifyAccounts(c *core.Context, uid int64, accounts []*models.Account) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -211,7 +256,7 @@ func (s *AccountService) ModifyAccounts(uid int64, accounts []*models.Account) e
accounts[i].UpdatedUnixTime = now
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(accounts); i++ {
account := accounts[i]
updatedRows, err := sess.ID(account.AccountId).Cols("name", "category", "icon", "color", "comment", "hidden", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(account)
@@ -228,7 +273,7 @@ func (s *AccountService) ModifyAccounts(uid int64, accounts []*models.Account) e
}
// HideAccount updates hidden field of given accounts
func (s *AccountService) HideAccount(uid int64, ids []int64, hidden bool) error {
func (s *AccountService) HideAccount(c *core.Context, uid int64, ids []int64, hidden bool) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -240,7 +285,7 @@ func (s *AccountService) HideAccount(uid int64, ids []int64, hidden bool) error
UpdatedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.Cols("hidden", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).In("account_id", ids).Update(updateModel)
if err != nil {
@@ -254,7 +299,7 @@ func (s *AccountService) HideAccount(uid int64, ids []int64, hidden bool) error
}
// ModifyAccountDisplayOrders updates display order of given accounts
func (s *AccountService) ModifyAccountDisplayOrders(uid int64, accounts []*models.Account) error {
func (s *AccountService) ModifyAccountDisplayOrders(c *core.Context, uid int64, accounts []*models.Account) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -263,7 +308,7 @@ func (s *AccountService) ModifyAccountDisplayOrders(uid int64, accounts []*model
accounts[i].UpdatedUnixTime = time.Now().Unix()
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(accounts); i++ {
account := accounts[i]
updatedRows, err := sess.ID(account.AccountId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(account)
@@ -280,7 +325,7 @@ func (s *AccountService) ModifyAccountDisplayOrders(uid int64, accounts []*model
}
// DeleteAccount deletes an existed account from database
func (s *AccountService) DeleteAccount(uid int64, accountId int64) error {
func (s *AccountService) DeleteAccount(c *core.Context, uid int64, accountId int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -293,7 +338,7 @@ func (s *AccountService) DeleteAccount(uid int64, accountId int64) error {
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
var accountAndSubAccounts []*models.Account
err := sess.Where("uid=? AND deleted=? AND (account_id=? OR parent_account_id=?)", uid, false, accountId, accountId).Find(&accountAndSubAccounts)
+21
View File
@@ -2,6 +2,8 @@ package services
import (
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
@@ -36,6 +38,20 @@ func (s *ServiceUsingConfig) CurrentConfig() *settings.Config {
return s.container.Current
}
// ServiceUsingMailer represents a service that need to use mailer
type ServiceUsingMailer struct {
container *mail.MailerContainer
}
// SendMail sends an email according to argument
func (s *ServiceUsingMailer) SendMail(message *mail.MailMessage) error {
if s.container.Current == nil {
return errs.ErrSMTPServerNotEnabled
}
return s.container.Current.SendMail(message)
}
// ServiceUsingUuid represents a service that need to use uuid
type ServiceUsingUuid struct {
container *uuid.UuidContainer
@@ -45,3 +61,8 @@ type ServiceUsingUuid struct {
func (s *ServiceUsingUuid) GenerateUuid(uuidType uuid.UuidType) int64 {
return s.container.GenerateUuid(uuidType)
}
// GenerateUuids generates new uuids according to given uuid type and count
func (s *ServiceUsingUuid) GenerateUuids(uuidType uuid.UuidType, count uint8) []int64 {
return s.container.GenerateUuids(uuidType, count)
}
+89
View File
@@ -0,0 +1,89 @@
package services
import (
"bytes"
"fmt"
"net/url"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/templates"
)
const passwordResetUrlFormat = "%sdesktop/#/resetpassword?token=%s"
// ForgetPasswordService represents forget password service
type ForgetPasswordService struct {
ServiceUsingConfig
ServiceUsingMailer
}
// Initialize a forget password service singleton instance
var (
ForgetPasswords = &ForgetPasswordService{
ServiceUsingConfig: ServiceUsingConfig{
container: settings.Container,
},
ServiceUsingMailer: ServiceUsingMailer{
container: mail.Container,
},
}
)
// SendPasswordResetEmail sends password reset email according to specified parameters
func (s *ForgetPasswordService) SendPasswordResetEmail(c *core.Context, user *models.User, passwordResetToken string, backupLocale string) error {
if !s.CurrentConfig().EnableSMTP {
return errs.ErrSMTPServerNotEnabled
}
locale := user.Language
if locale == "" {
locale = backupLocale
}
localeTextItems := locales.GetLocaleTextItems(locale)
forgetPasswordTextItems := localeTextItems.ForgetPasswordMailTextItems
expireTimeInMinutes := s.CurrentConfig().PasswordResetTokenExpiredTimeDuration.Minutes()
passwordResetUrl := fmt.Sprintf(passwordResetUrlFormat, s.CurrentConfig().RootUrl, url.QueryEscape(passwordResetToken))
tmpl, err := templates.GetTemplate(templates.TEMPLATE_PASSWORD_RESET)
if err != nil {
return err
}
templateParams := map[string]any{
"AppName": s.CurrentConfig().AppName,
"ForgetPasswordMail": map[string]any{
"Title": forgetPasswordTextItems.Title,
"Salutation": fmt.Sprintf(forgetPasswordTextItems.SalutationFormat, user.Nickname),
"DescriptionAboveBtn": forgetPasswordTextItems.DescriptionAboveBtn,
"ResetPasswordUrl": passwordResetUrl,
"ResetPassword": forgetPasswordTextItems.ResetPassword,
"DescriptionBelowBtn": fmt.Sprintf(forgetPasswordTextItems.DescriptionBelowBtnFormat, expireTimeInMinutes),
},
}
var bodyBuffer bytes.Buffer
err = tmpl.Execute(&bodyBuffer, templateParams)
if err != nil {
return err
}
message := &mail.MailMessage{
To: user.Email,
Subject: forgetPasswordTextItems.Title,
Body: bodyBuffer.String(),
}
err = s.SendMail(message)
return err
}
+132 -75
View File
@@ -6,8 +6,8 @@ import (
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v5/request"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -38,19 +38,19 @@ var (
)
// GetAllTokensByUid returns all token models of given user
func (s *TokenService) GetAllTokensByUid(uid int64) ([]*models.TokenRecord, error) {
func (s *TokenService) GetAllTokensByUid(c *core.Context, uid int64) ([]*models.TokenRecord, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var tokenRecords []*models.TokenRecord
err := s.TokenDB(uid).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time").Where("uid=?", uid).Find(&tokenRecords)
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time").Where("uid=?", uid).Find(&tokenRecords)
return tokenRecords, err
}
// GetAllUnexpiredNormalTokensByUid returns all available token models of given user
func (s *TokenService) GetAllUnexpiredNormalTokensByUid(uid int64) ([]*models.TokenRecord, error) {
func (s *TokenService) GetAllUnexpiredNormalTokensByUid(c *core.Context, uid int64) ([]*models.TokenRecord, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -58,66 +58,48 @@ func (s *TokenService) GetAllUnexpiredNormalTokensByUid(uid int64) ([]*models.To
now := time.Now().Unix()
var tokenRecords []*models.TokenRecord
err := s.TokenDB(uid).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords)
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords)
return tokenRecords, err
}
// ParseToken returns the token model according to request data
func (s *TokenService) ParseToken(c *core.Context) (*jwt.Token, *core.UserTokenClaims, error) {
claims := &core.UserTokenClaims{}
// ParseTokenByHeader returns the token model according to request data
func (s *TokenService) ParseTokenByHeader(c *core.Context) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, request.BearerExtractor{})
}
token, err := request.ParseFromRequest(c.Request, request.AuthorizationHeaderExtractor,
func(token *jwt.Token) (interface{}, error) {
uid, err := utils.StringToInt64(claims.Id)
now := time.Now().Unix()
// ParseTokenByArgument returns the token model according to request data
func (s *TokenService) ParseTokenByArgument(c *core.Context, tokenParameterName string) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, request.ArgumentExtractor{tokenParameterName})
}
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] user \"uid:%s\" in token is invalid, because %s", claims.Id, err.Error())
return nil, errs.ErrInvalidToken
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" in token of user \"uid:%s\" is invalid, because %s", claims.UserTokenId, claims.Id, err.Error())
return nil, errs.ErrInvalidUserTokenId
}
tokenRecord, err := s.getTokenRecord(uid, userTokenId, claims.IssuedAt)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%s\" record not found, because %s", claims.UserTokenId, claims.Id, err.Error())
return nil, errs.ErrTokenRecordNotFound
}
if tokenRecord.ExpiredUnixTime < now {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%s\" record is expired", claims.UserTokenId, claims.Id)
return nil, errs.ErrTokenExpired
}
return []byte(tokenRecord.Secret), nil
}, request.WithClaims(claims))
if err != nil {
return nil, nil, err
}
return token, claims, err
// ParseTokenByCookie returns the token model according to request data
func (s *TokenService) ParseTokenByCookie(c *core.Context, tokenCookieName string) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, utils.CookieExtractor{tokenCookieName})
}
// CreateToken generates a new normal token and saves to database
func (s *TokenService) CreateToken(user *models.User, ctx *core.Context) (string, *core.UserTokenClaims, error) {
return s.createToken(user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(ctx), s.CurrentConfig().TokenExpiredTimeDuration)
func (s *TokenService) CreateToken(c *core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
return s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(c), s.CurrentConfig().TokenExpiredTimeDuration)
}
// CreateRequire2FAToken generates a new token requiring user to verify 2fa passcode and saves to database
func (s *TokenService) CreateRequire2FAToken(user *models.User, ctx *core.Context) (string, *core.UserTokenClaims, error) {
return s.createToken(user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(ctx), s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
func (s *TokenService) CreateRequire2FAToken(c *core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
return s.createToken(c, user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(c), s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
}
// CreateEmailVerifyToken generates a new email verify token and saves to database
func (s *TokenService) CreateEmailVerifyToken(c *core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
return s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, s.getUserAgent(c), s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration)
}
// CreatePasswordResetToken generates a new password reset token and saves to database
func (s *TokenService) CreatePasswordResetToken(c *core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
return s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
}
// DeleteToken deletes given token from database
func (s *TokenService) DeleteToken(tokenRecord *models.TokenRecord) error {
func (s *TokenService) DeleteToken(c *core.Context, tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -126,7 +108,7 @@ func (s *TokenService) DeleteToken(tokenRecord *models.TokenRecord) error {
return errs.ErrInvalidUserTokenId
}
return s.TokenDB(tokenRecord.Uid).DoTransaction(func(sess *xorm.Session) error {
return s.TokenDB(tokenRecord.Uid).DoTransaction(c, func(sess *xorm.Session) error {
deletedRows, err := sess.Where("uid=? AND user_token_id=? AND created_unix_time=?", tokenRecord.Uid, tokenRecord.UserTokenId, tokenRecord.CreatedUnixTime).Delete(&models.TokenRecord{})
if err != nil {
@@ -140,12 +122,12 @@ func (s *TokenService) DeleteToken(tokenRecord *models.TokenRecord) error {
}
// DeleteTokens deletes given tokens from database
func (s *TokenService) DeleteTokens(uid int64, tokenRecords []*models.TokenRecord) error {
func (s *TokenService) DeleteTokens(c *core.Context, uid int64, tokenRecords []*models.TokenRecord) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.TokenDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.TokenDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(tokenRecords); i++ {
tokenRecord := tokenRecords[i]
deletedRows, err := sess.Where("uid=? AND user_token_id=? AND created_unix_time=?", uid, tokenRecord.UserTokenId, tokenRecord.CreatedUnixTime).Delete(&models.TokenRecord{})
@@ -162,34 +144,55 @@ func (s *TokenService) DeleteTokens(uid int64, tokenRecords []*models.TokenRecor
}
// DeleteTokenByClaims deletes given token from database
func (s *TokenService) DeleteTokenByClaims(claims *core.UserTokenClaims) error {
uid, err := utils.StringToInt64(claims.Id)
if err != nil {
return errs.ErrUserIdInvalid
}
func (s *TokenService) DeleteTokenByClaims(c *core.Context, claims *core.UserTokenClaims) error {
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
return errs.ErrInvalidUserTokenId
}
return s.DeleteToken(&models.TokenRecord{Uid: uid, UserTokenId: userTokenId, CreatedUnixTime: claims.IssuedAt})
return s.DeleteToken(c, &models.TokenRecord{
Uid: claims.Uid,
UserTokenId: userTokenId,
CreatedUnixTime: claims.IssuedAt,
})
}
// DeleteTokensBeforeTime deletes tokens that is created before specific tim
func (s *TokenService) DeleteTokensBeforeTime(uid int64, expireTime int64) error {
// DeleteTokensBeforeTime deletes tokens that is created before specific time
func (s *TokenService) DeleteTokensBeforeTime(c *core.Context, uid int64, expireTime int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.TokenDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.TokenDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Where("uid=? AND created_unix_time<?", uid, expireTime).Delete(&models.TokenRecord{})
return err
})
}
// DeleteTokensByType deletes specified type tokens
func (s *TokenService) DeleteTokensByType(c *core.Context, uid int64, tokenType core.TokenType) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.TokenDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Where("uid=? AND token_type=?", uid, tokenType).Delete(&models.TokenRecord{})
return err
})
}
// ExistsValidTokenByType returns whether the given token type exists
func (s *TokenService) ExistsValidTokenByType(c *core.Context, uid int64, tokenType core.TokenType) (bool, error) {
if uid <= 0 {
return false, errs.ErrUserIdInvalid
}
now := time.Now().Unix()
return s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "expired_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, tokenType, now).Exist(&models.TokenRecord{})
}
// ParseFromTokenId returns token model according to token id
func (s *TokenService) ParseFromTokenId(tokenId string) (*models.TokenRecord, error) {
pairs := strings.Split(tokenId, ":")
@@ -230,7 +233,63 @@ func (s *TokenService) GenerateTokenId(tokenRecord *models.TokenRecord) string {
return fmt.Sprintf("%d:%d:%d", tokenRecord.Uid, tokenRecord.CreatedUnixTime, tokenRecord.UserTokenId)
}
func (s *TokenService) createToken(user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, error) {
func (s *TokenService) parseToken(c *core.Context, extractor request.Extractor) (*jwt.Token, *core.UserTokenClaims, error) {
claims := &core.UserTokenClaims{}
token, err := request.ParseFromRequest(c.Request, extractor,
func(token *jwt.Token) (any, error) {
now := time.Now().Unix()
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" in token of user \"uid:%d\" is invalid, because %s", claims.UserTokenId, claims.Uid, err.Error())
return nil, errs.ErrInvalidUserTokenId
}
tokenRecord, err := s.getTokenRecord(c, claims.Uid, userTokenId, claims.IssuedAt)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%d\" record not found, because %s", claims.UserTokenId, claims.Uid, err.Error())
return nil, errs.ErrTokenRecordNotFound
}
if tokenRecord.ExpiredUnixTime < now {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%d\" record is expired", claims.UserTokenId, claims.Uid)
return nil, errs.ErrTokenExpired
}
return []byte(tokenRecord.Secret), nil
},
request.WithClaims(claims),
request.WithParser(jwt.NewParser(jwt.WithIssuedAt())),
)
if err != nil {
if err == request.ErrNoTokenInRequest {
return nil, nil, errs.ErrTokenIsEmpty
}
if err == jwt.ErrTokenMalformed || err == jwt.ErrTokenUnverifiable || err == jwt.ErrTokenSignatureInvalid {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token is invalid, because %s", err.Error())
return nil, nil, errs.ErrCurrentInvalidToken
}
if err == jwt.ErrTokenExpired {
return nil, nil, errs.ErrCurrentTokenExpired
}
if err == jwt.ErrTokenUsedBeforeIssued {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token is invalid, because issue time is later than now")
return nil, nil, errs.ErrCurrentInvalidToken
}
return nil, nil, err
}
return token, claims, err
}
func (s *TokenService) createToken(c *core.Context, user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, error) {
var err error
now := time.Now()
@@ -249,13 +308,11 @@ func (s *TokenService) createToken(user *models.User, tokenType core.TokenType,
claims := &core.UserTokenClaims{
UserTokenId: utils.Int64ToString(tokenRecord.UserTokenId),
Uid: tokenRecord.Uid,
Username: user.Username,
Type: tokenRecord.TokenType,
StandardClaims: jwt.StandardClaims{
Id: utils.Int64ToString(tokenRecord.Uid),
IssuedAt: tokenRecord.CreatedUnixTime,
ExpiresAt: tokenRecord.ExpiredUnixTime,
},
IssuedAt: tokenRecord.CreatedUnixTime,
ExpiresAt: tokenRecord.ExpiredUnixTime,
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@@ -265,7 +322,7 @@ func (s *TokenService) createToken(user *models.User, tokenType core.TokenType,
return "", nil, err
}
err = s.createTokenRecord(tokenRecord)
err = s.createTokenRecord(c, tokenRecord)
if err != nil {
return "", nil, err
@@ -274,7 +331,7 @@ func (s *TokenService) createToken(user *models.User, tokenType core.TokenType,
return tokenString, claims, err
}
func (s *TokenService) getTokenRecord(uid int64, userTokenId int64, createUnixTime int64) (*models.TokenRecord, error) {
func (s *TokenService) getTokenRecord(c *core.Context, uid int64, userTokenId int64, createUnixTime int64) (*models.TokenRecord, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -284,7 +341,7 @@ func (s *TokenService) getTokenRecord(uid int64, userTokenId int64, createUnixTi
}
tokenRecord := &models.TokenRecord{}
has, err := s.TokenDB(uid).Where("uid=? AND user_token_id=? AND created_unix_time=?", uid, userTokenId, createUnixTime).Limit(1).Get(tokenRecord)
has, err := s.TokenDB(uid).NewSession(c).Where("uid=? AND user_token_id=? AND created_unix_time=?", uid, userTokenId, createUnixTime).Limit(1).Get(tokenRecord)
if err != nil {
return nil, err
@@ -297,7 +354,7 @@ func (s *TokenService) getTokenRecord(uid int64, userTokenId int64, createUnixTi
return tokenRecord, nil
}
func (s *TokenService) createTokenRecord(tokenRecord *models.TokenRecord) error {
func (s *TokenService) createTokenRecord(c *core.Context, tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -306,7 +363,7 @@ func (s *TokenService) createTokenRecord(tokenRecord *models.TokenRecord) error
return errs.ErrInvalidUserTokenId
}
return s.TokenDB(tokenRecord.Uid).DoTransaction(func(sess *xorm.Session) error {
return s.TokenDB(tokenRecord.Uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Insert(tokenRecord)
return err
})
+50 -25
View File
@@ -5,6 +5,7 @@ import (
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -29,14 +30,25 @@ var (
}
)
// GetTotalCategoryCountByUid returns total category count of user
func (s *TransactionCategoryService) GetTotalCategoryCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Count(&models.TransactionCategory{})
return count, err
}
// GetAllCategoriesByUid returns all transaction category models of user
func (s *TransactionCategoryService) GetAllCategoriesByUid(uid int64, categoryType models.TransactionCategoryType, parentCategoryId int64) ([]*models.TransactionCategory, error) {
func (s *TransactionCategoryService) GetAllCategoriesByUid(c *core.Context, uid int64, categoryType models.TransactionCategoryType, parentCategoryId int64) ([]*models.TransactionCategory, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
condition := "uid=? AND deleted=?"
conditionParams := make([]interface{}, 0, 8)
conditionParams := make([]any, 0, 8)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
@@ -51,13 +63,13 @@ func (s *TransactionCategoryService) GetAllCategoriesByUid(uid int64, categoryTy
}
var categories []*models.TransactionCategory
err := s.UserDataDB(uid).Where(condition, conditionParams...).OrderBy("type asc, parent_category_id asc, display_order asc").Find(&categories)
err := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).OrderBy("type asc, parent_category_id asc, display_order asc").Find(&categories)
return categories, err
}
// GetCategoryByCategoryId returns a transaction category model according to transaction category id
func (s *TransactionCategoryService) GetCategoryByCategoryId(uid int64, categoryId int64) (*models.TransactionCategory, error) {
func (s *TransactionCategoryService) GetCategoryByCategoryId(c *core.Context, uid int64, categoryId int64) (*models.TransactionCategory, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -67,7 +79,7 @@ func (s *TransactionCategoryService) GetCategoryByCategoryId(uid int64, category
}
category := &models.TransactionCategory{}
has, err := s.UserDataDB(uid).ID(categoryId).Where("uid=? AND deleted=?", uid, false).Get(category)
has, err := s.UserDataDB(uid).NewSession(c).ID(categoryId).Where("uid=? AND deleted=?", uid, false).Get(category)
if err != nil {
return nil, err
@@ -79,7 +91,7 @@ func (s *TransactionCategoryService) GetCategoryByCategoryId(uid int64, category
}
// GetCategoriesByCategoryIds returns transaction category models according to transaction category ids
func (s *TransactionCategoryService) GetCategoriesByCategoryIds(uid int64, categoryIds []int64) (map[int64]*models.TransactionCategory, error) {
func (s *TransactionCategoryService) GetCategoriesByCategoryIds(c *core.Context, uid int64, categoryIds []int64) (map[int64]*models.TransactionCategory, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -89,7 +101,7 @@ func (s *TransactionCategoryService) GetCategoriesByCategoryIds(uid int64, categ
}
var categories []*models.TransactionCategory
err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).In("category_id", categoryIds).Find(&categories)
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("category_id", categoryIds).Find(&categories)
if err != nil {
return nil, err
@@ -100,13 +112,13 @@ func (s *TransactionCategoryService) GetCategoriesByCategoryIds(uid int64, categ
}
// GetMaxDisplayOrder returns the max display order according to transaction category type
func (s *TransactionCategoryService) GetMaxDisplayOrder(uid int64, categoryType models.TransactionCategoryType) (int, error) {
func (s *TransactionCategoryService) GetMaxDisplayOrder(c *core.Context, uid int64, categoryType models.TransactionCategoryType) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
category := &models.TransactionCategory{}
has, err := s.UserDataDB(uid).Cols("uid", "deleted", "parent_category_id", "display_order").Where("uid=? AND deleted=? AND type=? AND parent_category_id=?", uid, false, categoryType, models.LevelOneTransactionParentId).OrderBy("display_order desc").Limit(1).Get(category)
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "parent_category_id", "display_order").Where("uid=? AND deleted=? AND type=? AND parent_category_id=?", uid, false, categoryType, models.LevelOneTransactionParentId).OrderBy("display_order desc").Limit(1).Get(category)
if err != nil {
return 0, err
@@ -120,7 +132,7 @@ func (s *TransactionCategoryService) GetMaxDisplayOrder(uid int64, categoryType
}
// GetMaxSubCategoryDisplayOrder returns the max display order of sub transaction category according to transaction category type and parent transaction category id
func (s *TransactionCategoryService) GetMaxSubCategoryDisplayOrder(uid int64, categoryType models.TransactionCategoryType, parentCategoryId int64) (int, error) {
func (s *TransactionCategoryService) GetMaxSubCategoryDisplayOrder(c *core.Context, uid int64, categoryType models.TransactionCategoryType, parentCategoryId int64) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
@@ -130,7 +142,7 @@ func (s *TransactionCategoryService) GetMaxSubCategoryDisplayOrder(uid int64, ca
}
category := &models.TransactionCategory{}
has, err := s.UserDataDB(uid).Cols("uid", "deleted", "parent_category_id", "display_order").Where("uid=? AND deleted=? AND type=? AND parent_category_id=?", uid, false, categoryType, parentCategoryId).OrderBy("display_order desc").Limit(1).Get(category)
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "parent_category_id", "display_order").Where("uid=? AND deleted=? AND type=? AND parent_category_id=?", uid, false, categoryType, parentCategoryId).OrderBy("display_order desc").Limit(1).Get(category)
if err != nil {
return 0, err
@@ -144,25 +156,29 @@ func (s *TransactionCategoryService) GetMaxSubCategoryDisplayOrder(uid int64, ca
}
// CreateCategory saves a new transaction category model to database
func (s *TransactionCategoryService) CreateCategory(category *models.TransactionCategory) error {
func (s *TransactionCategoryService) CreateCategory(c *core.Context, category *models.TransactionCategory) error {
if category.Uid <= 0 {
return errs.ErrUserIdInvalid
}
category.CategoryId = s.GenerateUuid(uuid.UUID_TYPE_CATEGORY)
if category.CategoryId < 1 {
return errs.ErrSystemIsBusy
}
category.Deleted = false
category.CreatedUnixTime = time.Now().Unix()
category.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(category.Uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(category.Uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Insert(category)
return err
})
}
// CreateCategories saves a few transaction category models to database
func (s *TransactionCategoryService) CreateCategories(uid int64, categories map[*models.TransactionCategory][]*models.TransactionCategory) ([]*models.TransactionCategory, error) {
func (s *TransactionCategoryService) CreateCategories(c *core.Context, uid int64, categories map[*models.TransactionCategory][]*models.TransactionCategory) ([]*models.TransactionCategory, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -174,6 +190,10 @@ func (s *TransactionCategoryService) CreateCategories(uid int64, categories map[
primaryCategory := primaryCategories[i]
primaryCategory.CategoryId = s.GenerateUuid(uuid.UUID_TYPE_CATEGORY)
if primaryCategory.CategoryId < 1 {
return nil, errs.ErrSystemIsBusy
}
primaryCategory.Deleted = false
primaryCategory.CreatedUnixTime = time.Now().Unix()
primaryCategory.UpdatedUnixTime = time.Now().Unix()
@@ -185,6 +205,11 @@ func (s *TransactionCategoryService) CreateCategories(uid int64, categories map[
for j := 0; j < len(secondaryCategories); j++ {
secondaryCategory := secondaryCategories[j]
secondaryCategory.CategoryId = s.GenerateUuid(uuid.UUID_TYPE_CATEGORY)
if secondaryCategory.CategoryId < 1 {
return nil, errs.ErrSystemIsBusy
}
secondaryCategory.ParentCategoryId = primaryCategory.CategoryId
secondaryCategory.Deleted = false
@@ -195,7 +220,7 @@ func (s *TransactionCategoryService) CreateCategories(uid int64, categories map[
}
}
err := s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
err := s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(allCategories); i++ {
category := allCategories[i]
_, err := sess.Insert(category)
@@ -216,14 +241,14 @@ func (s *TransactionCategoryService) CreateCategories(uid int64, categories map[
}
// ModifyCategory saves an existed transaction category model to database
func (s *TransactionCategoryService) ModifyCategory(category *models.TransactionCategory) error {
func (s *TransactionCategoryService) ModifyCategory(c *core.Context, category *models.TransactionCategory) error {
if category.Uid <= 0 {
return errs.ErrUserIdInvalid
}
category.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(category.Uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(category.Uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.ID(category.CategoryId).Cols("name", "icon", "color", "comment", "hidden", "updated_unix_time").Where("uid=? AND deleted=?", category.Uid, false).Update(category)
if err != nil {
@@ -237,7 +262,7 @@ func (s *TransactionCategoryService) ModifyCategory(category *models.Transaction
}
// HideCategory updates hidden field of given transaction categories
func (s *TransactionCategoryService) HideCategory(uid int64, ids []int64, hidden bool) error {
func (s *TransactionCategoryService) HideCategory(c *core.Context, uid int64, ids []int64, hidden bool) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -249,7 +274,7 @@ func (s *TransactionCategoryService) HideCategory(uid int64, ids []int64, hidden
UpdatedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.Cols("hidden", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).In("category_id", ids).Update(updateModel)
if err != nil {
@@ -263,7 +288,7 @@ func (s *TransactionCategoryService) HideCategory(uid int64, ids []int64, hidden
}
// ModifyCategoryDisplayOrders updates display order of given transaction categories
func (s *TransactionCategoryService) ModifyCategoryDisplayOrders(uid int64, categories []*models.TransactionCategory) error {
func (s *TransactionCategoryService) ModifyCategoryDisplayOrders(c *core.Context, uid int64, categories []*models.TransactionCategory) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -272,7 +297,7 @@ func (s *TransactionCategoryService) ModifyCategoryDisplayOrders(uid int64, cate
categories[i].UpdatedUnixTime = time.Now().Unix()
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(categories); i++ {
category := categories[i]
updatedRows, err := sess.ID(category.CategoryId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(category)
@@ -289,7 +314,7 @@ func (s *TransactionCategoryService) ModifyCategoryDisplayOrders(uid int64, cate
}
// DeleteCategory deletes an existed transaction category from database
func (s *TransactionCategoryService) DeleteCategory(uid int64, categoryId int64) error {
func (s *TransactionCategoryService) DeleteCategory(c *core.Context, uid int64, categoryId int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -301,7 +326,7 @@ func (s *TransactionCategoryService) DeleteCategory(uid int64, categoryId int64)
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
var categoryAndSubCategories []*models.TransactionCategory
err := sess.Where("uid=? AND deleted=? AND (category_id=? OR parent_category_id=?)", uid, false, categoryId, categoryId).Find(&categoryAndSubCategories)
@@ -338,7 +363,7 @@ func (s *TransactionCategoryService) DeleteCategory(uid int64, categoryId int64)
}
// DeleteAllCategories deletes all existed transaction categories from database
func (s *TransactionCategoryService) DeleteAllCategories(uid int64) error {
func (s *TransactionCategoryService) DeleteAllCategories(c *core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -350,7 +375,7 @@ func (s *TransactionCategoryService) DeleteAllCategories(uid int64) error {
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
exists, err := sess.Cols("uid", "deleted", "category_id").Where("uid=? AND deleted=? AND category_id<>?", uid, false, 0).Limit(1).Exist(&models.Transaction{})
if err != nil {
+44 -28
View File
@@ -5,6 +5,7 @@ import (
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -29,20 +30,31 @@ var (
}
)
// GetTotalTagCountByUid returns total tag count of user
func (s *TransactionTagService) GetTotalTagCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Count(&models.TransactionTag{})
return count, err
}
// GetAllTagsByUid returns all transaction tag models of user
func (s *TransactionTagService) GetAllTagsByUid(uid int64) ([]*models.TransactionTag, error) {
func (s *TransactionTagService) GetAllTagsByUid(c *core.Context, uid int64) ([]*models.TransactionTag, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var tags []*models.TransactionTag
err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).Find(&tags)
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Find(&tags)
return tags, err
}
// GetTagByTagId returns a transaction tag model according to transaction tag id
func (s *TransactionTagService) GetTagByTagId(uid int64, tagId int64) (*models.TransactionTag, error) {
func (s *TransactionTagService) GetTagByTagId(c *core.Context, uid int64, tagId int64) (*models.TransactionTag, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -52,7 +64,7 @@ func (s *TransactionTagService) GetTagByTagId(uid int64, tagId int64) (*models.T
}
tag := &models.TransactionTag{}
has, err := s.UserDataDB(uid).ID(tagId).Where("uid=? AND deleted=?", uid, false).Get(tag)
has, err := s.UserDataDB(uid).NewSession(c).ID(tagId).Where("uid=? AND deleted=?", uid, false).Get(tag)
if err != nil {
return nil, err
@@ -64,7 +76,7 @@ func (s *TransactionTagService) GetTagByTagId(uid int64, tagId int64) (*models.T
}
// GetTagsByTagIds returns transaction tag models according to transaction tag ids
func (s *TransactionTagService) GetTagsByTagIds(uid int64, tagIds []int64) (map[int64]*models.TransactionTag, error) {
func (s *TransactionTagService) GetTagsByTagIds(c *core.Context, uid int64, tagIds []int64) (map[int64]*models.TransactionTag, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -74,7 +86,7 @@ func (s *TransactionTagService) GetTagsByTagIds(uid int64, tagIds []int64) (map[
}
var tags []*models.TransactionTag
err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).In("tag_id", tagIds).Find(&tags)
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("tag_id", tagIds).Find(&tags)
if err != nil {
return nil, err
@@ -85,13 +97,13 @@ func (s *TransactionTagService) GetTagsByTagIds(uid int64, tagIds []int64) (map[
}
// GetMaxDisplayOrder returns the max display order
func (s *TransactionTagService) GetMaxDisplayOrder(uid int64) (int, error) {
func (s *TransactionTagService) GetMaxDisplayOrder(c *core.Context, uid int64) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
tag := &models.TransactionTag{}
has, err := s.UserDataDB(uid).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=?", uid, false).OrderBy("display_order desc").Limit(1).Get(tag)
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=?", uid, false).OrderBy("display_order desc").Limit(1).Get(tag)
if err != nil {
return 0, err
@@ -105,13 +117,13 @@ func (s *TransactionTagService) GetMaxDisplayOrder(uid int64) (int, error) {
}
// GetAllTagIdsOfAllTransactions returns all transaction tag ids
func (s *TransactionTagService) GetAllTagIdsOfAllTransactions(uid int64) (map[int64][]int64, error) {
func (s *TransactionTagService) GetAllTagIdsOfAllTransactions(c *core.Context, uid int64) (map[int64][]int64, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var tagIndexs []*models.TransactionTagIndex
err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).Find(&tagIndexs)
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Find(&tagIndexs)
allTransactionTagIds := s.getGroupedTransactionTagIds(tagIndexs)
@@ -119,13 +131,13 @@ func (s *TransactionTagService) GetAllTagIdsOfAllTransactions(uid int64) (map[in
}
// GetAllTagIdsOfTransactions returns transaction tag ids for given transactions
func (s *TransactionTagService) GetAllTagIdsOfTransactions(uid int64, transactionIds []int64) (map[int64][]int64, error) {
func (s *TransactionTagService) GetAllTagIdsOfTransactions(c *core.Context, uid int64, transactionIds []int64) (map[int64][]int64, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var tagIndexs []*models.TransactionTagIndex
err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).Find(&tagIndexs)
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).Find(&tagIndexs)
allTransactionTagIds := s.getGroupedTransactionTagIds(tagIndexs)
@@ -133,12 +145,12 @@ func (s *TransactionTagService) GetAllTagIdsOfTransactions(uid int64, transactio
}
// CreateTag saves a new transaction tag model to database
func (s *TransactionTagService) CreateTag(tag *models.TransactionTag) error {
func (s *TransactionTagService) CreateTag(c *core.Context, tag *models.TransactionTag) error {
if tag.Uid <= 0 {
return errs.ErrUserIdInvalid
}
exists, err := s.ExistsTagName(tag.Uid, tag.Name)
exists, err := s.ExistsTagName(c, tag.Uid, tag.Name)
if err != nil {
return err
@@ -148,23 +160,27 @@ func (s *TransactionTagService) CreateTag(tag *models.TransactionTag) error {
tag.TagId = s.GenerateUuid(uuid.UUID_TYPE_TAG)
if tag.TagId < 1 {
return errs.ErrSystemIsBusy
}
tag.Deleted = false
tag.CreatedUnixTime = time.Now().Unix()
tag.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(tag.Uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(tag.Uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Insert(tag)
return err
})
}
// ModifyTag saves an existed transaction tag model to database
func (s *TransactionTagService) ModifyTag(tag *models.TransactionTag) error {
func (s *TransactionTagService) ModifyTag(c *core.Context, tag *models.TransactionTag) error {
if tag.Uid <= 0 {
return errs.ErrUserIdInvalid
}
exists, err := s.ExistsTagName(tag.Uid, tag.Name)
exists, err := s.ExistsTagName(c, tag.Uid, tag.Name)
if err != nil {
return err
@@ -174,7 +190,7 @@ func (s *TransactionTagService) ModifyTag(tag *models.TransactionTag) error {
tag.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(tag.Uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(tag.Uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.ID(tag.TagId).Cols("name", "updated_unix_time").Where("uid=? AND deleted=?", tag.Uid, false).Update(tag)
if err != nil {
@@ -188,7 +204,7 @@ func (s *TransactionTagService) ModifyTag(tag *models.TransactionTag) error {
}
// HideTag updates hidden field of given transaction tags
func (s *TransactionTagService) HideTag(uid int64, ids []int64, hidden bool) error {
func (s *TransactionTagService) HideTag(c *core.Context, uid int64, ids []int64, hidden bool) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -200,7 +216,7 @@ func (s *TransactionTagService) HideTag(uid int64, ids []int64, hidden bool) err
UpdatedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.Cols("hidden", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).In("tag_id", ids).Update(updateModel)
if err != nil {
@@ -214,7 +230,7 @@ func (s *TransactionTagService) HideTag(uid int64, ids []int64, hidden bool) err
}
// ModifyTagDisplayOrders updates display order of given transaction tags
func (s *TransactionTagService) ModifyTagDisplayOrders(uid int64, tags []*models.TransactionTag) error {
func (s *TransactionTagService) ModifyTagDisplayOrders(c *core.Context, uid int64, tags []*models.TransactionTag) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -223,7 +239,7 @@ func (s *TransactionTagService) ModifyTagDisplayOrders(uid int64, tags []*models
tags[i].UpdatedUnixTime = time.Now().Unix()
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(tags); i++ {
tag := tags[i]
updatedRows, err := sess.ID(tag.TagId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(tag)
@@ -240,7 +256,7 @@ func (s *TransactionTagService) ModifyTagDisplayOrders(uid int64, tags []*models
}
// DeleteTag deletes an existed transaction tag from database
func (s *TransactionTagService) DeleteTag(uid int64, tagId int64) error {
func (s *TransactionTagService) DeleteTag(c *core.Context, uid int64, tagId int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -252,7 +268,7 @@ func (s *TransactionTagService) DeleteTag(uid int64, tagId int64) error {
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
exists, err := sess.Cols("uid", "tag_id").Where("uid=? AND deleted=? AND tag_id=?", uid, false, tagId).Limit(1).Exist(&models.TransactionTagIndex{})
if err != nil {
@@ -274,7 +290,7 @@ func (s *TransactionTagService) DeleteTag(uid int64, tagId int64) error {
}
// DeleteAllTags deletes all existed transaction tags from database
func (s *TransactionTagService) DeleteAllTags(uid int64) error {
func (s *TransactionTagService) DeleteAllTags(c *core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -286,7 +302,7 @@ func (s *TransactionTagService) DeleteAllTags(uid int64) error {
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
exists, err := sess.Cols("uid", "deleted").Where("uid=? AND deleted=?", uid, false).Limit(1).Exist(&models.TransactionTagIndex{})
if err != nil {
@@ -306,12 +322,12 @@ func (s *TransactionTagService) DeleteAllTags(uid int64) error {
}
// ExistsTagName returns whether the given tag name exists
func (s *TransactionTagService) ExistsTagName(uid int64, name string) (bool, error) {
func (s *TransactionTagService) ExistsTagName(c *core.Context, uid int64, name string) (bool, error) {
if name == "" {
return false, errs.ErrTransactionTagNameIsEmpty
}
return s.UserDataDB(uid).Cols("name").Where("uid=? AND deleted=? AND name=?", uid, false, name).Exist(&models.TransactionTag{})
return s.UserDataDB(uid).NewSession(c).Cols("name").Where("uid=? AND deleted=? AND name=?", uid, false, name).Exist(&models.TransactionTag{})
}
// GetTagMapByList returns a transaction tag map by a list
+142 -64
View File
@@ -7,6 +7,7 @@ import (
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -32,13 +33,24 @@ var (
}
)
// GetTotalTransactionCountByUid returns total transaction count of user
func (s *TransactionService) GetTotalTransactionCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Count(&models.Transaction{})
return count, err
}
// GetAllTransactions returns all transactions
func (s *TransactionService) GetAllTransactions(uid int64, pageCount int, noDuplicated bool) ([]*models.Transaction, error) {
func (s *TransactionService) GetAllTransactions(c *core.Context, uid int64, pageCount int32, noDuplicated bool) ([]*models.Transaction, error) {
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
var allTransactions []*models.Transaction
for maxTransactionTime > 0 {
transactions, err := s.GetAllTransactionsByMaxTime(uid, maxTransactionTime, pageCount, noDuplicated)
transactions, err := s.GetAllTransactionsByMaxTime(c, uid, maxTransactionTime, pageCount, noDuplicated)
if err != nil {
return nil, err
@@ -46,7 +58,7 @@ func (s *TransactionService) GetAllTransactions(uid int64, pageCount int, noDupl
allTransactions = append(allTransactions, transactions...)
if len(transactions) < pageCount {
if len(transactions) < int(pageCount) {
maxTransactionTime = 0
break
}
@@ -58,16 +70,22 @@ func (s *TransactionService) GetAllTransactions(uid int64, pageCount int, noDupl
}
// GetAllTransactionsByMaxTime returns all transactions before given time
func (s *TransactionService) GetAllTransactionsByMaxTime(uid int64, maxTransactionTime int64, count int, noDuplicated bool) ([]*models.Transaction, error) {
return s.GetTransactionsByMaxTime(uid, maxTransactionTime, 0, 0, nil, 0, "", count, noDuplicated)
func (s *TransactionService) GetAllTransactionsByMaxTime(c *core.Context, uid int64, maxTransactionTime int64, count int32, noDuplicated bool) ([]*models.Transaction, error) {
return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, "", 1, count, false, noDuplicated)
}
// GetTransactionsByMaxTime returns transactions before given time
func (s *TransactionService) GetTransactionsByMaxTime(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountId int64, keyword string, count int, noDuplicated bool) ([]*models.Transaction, error) {
func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if page < 0 {
return nil, errs.ErrPageIndexInvalid
} else if page == 0 {
page = 1
}
if count < 1 {
return nil, errs.ErrPageCountInvalid
}
@@ -75,47 +93,58 @@ func (s *TransactionService) GetTransactionsByMaxTime(uid int64, maxTransactionT
var transactions []*models.Transaction
var err error
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountId, keyword, noDuplicated)
err = s.UserDataDB(uid).Where(condition, conditionParams...).Limit(count, 0).OrderBy("transaction_time desc").Find(&transactions)
actualCount := count
if needOneMoreItem {
actualCount++
}
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, noDuplicated)
err = s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
return transactions, err
}
// GetTransactionsInMonthByPage returns transactions in given year and month
func (s *TransactionService) GetTransactionsInMonthByPage(uid int64, year int, month int, transactionType models.TransactionDbType, categoryIds []int64, accountId int64, keyword string, page int, count int, utcOffset int16) ([]*models.Transaction, error) {
// GetTransactionsInMonthByPage returns all transactions in given year and month
func (s *TransactionService) GetTransactionsInMonthByPage(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if page < 1 {
return nil, errs.ErrPageIndexInvalid
}
if count < 1 {
return nil, errs.ErrPageCountInvalid
}
startTime, err := utils.ParseFromLongDateTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month), utcOffset)
startMinUnixTime, err := utils.ParseFromLongDateTimeToMinUnixTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month))
startMaxUnixTime, err := utils.ParseFromLongDateTimeToMaxUnixTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month))
if err != nil {
return nil, errs.ErrSystemError
}
endTime := startTime.AddDate(0, 1, 0)
endMaxUnixTime := startMaxUnixTime.AddDate(0, 1, 0)
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startTime.Unix())
maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endTime.Unix()) - 1
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startMinUnixTime.Unix())
maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endMaxUnixTime.Unix()) - 1
var transactions []*models.Transaction
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountId, keyword, true)
err = s.UserDataDB(uid).Where(condition, conditionParams...).Limit(count, count*(page-1)).OrderBy("transaction_time desc").Find(&transactions)
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, true)
err = s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).OrderBy("transaction_time desc").Find(&transactions)
return transactions, err
transactionsInMonth := make([]*models.Transaction, 0, len(transactions))
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime)
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
if utils.IsUnixTimeEqualsYearAndMonth(transactionUnixTime, transactionTimeZone, year, month) {
transactionsInMonth = append(transactionsInMonth, transaction)
}
}
return transactionsInMonth, err
}
// GetTransactionByTransactionId returns a transaction model according to transaction id
func (s *TransactionService) GetTransactionByTransactionId(uid int64, transactionId int64) (*models.Transaction, error) {
func (s *TransactionService) GetTransactionByTransactionId(c *core.Context, uid int64, transactionId int64) (*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -125,7 +154,7 @@ func (s *TransactionService) GetTransactionByTransactionId(uid int64, transactio
}
transaction := &models.Transaction{}
has, err := s.UserDataDB(uid).ID(transactionId).Where("uid=? AND deleted=?", uid, false).Get(transaction)
has, err := s.UserDataDB(uid).NewSession(c).ID(transactionId).Where("uid=? AND deleted=?", uid, false).Get(transaction)
if err != nil {
return nil, err
@@ -137,12 +166,12 @@ func (s *TransactionService) GetTransactionByTransactionId(uid int64, transactio
}
// GetAllTransactionCount returns total count of transactions
func (s *TransactionService) GetAllTransactionCount(uid int64) (int64, error) {
return s.GetTransactionCount(uid, 0, 0, 0, nil, 0, "")
func (s *TransactionService) GetAllTransactionCount(c *core.Context, uid int64) (int64, error) {
return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, "")
}
// GetMonthTransactionCount returns total count of transactions in given year and month
func (s *TransactionService) GetMonthTransactionCount(uid int64, year int, month int, transactionType models.TransactionDbType, categoryIds []int64, accountId int64, keyword string, utcOffset int16) (int64, error) {
func (s *TransactionService) GetMonthTransactionCount(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, utcOffset int16) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
@@ -158,21 +187,21 @@ func (s *TransactionService) GetMonthTransactionCount(uid int64, year int, month
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startTime.Unix())
maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endTime.Unix()) - 1
return s.GetTransactionCount(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountId, keyword)
return s.GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword)
}
// GetTransactionCount returns count of transactions
func (s *TransactionService) GetTransactionCount(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountId int64, keyword string) (int64, error) {
func (s *TransactionService) GetTransactionCount(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountId, keyword, true)
return s.UserDataDB(uid).Where(condition, conditionParams...).Count(&models.Transaction{})
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, true)
return s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).Count(&models.Transaction{})
}
// CreateTransaction saves a new transaction to database
func (s *TransactionService) CreateTransaction(transaction *models.Transaction, tagIds []int64) error {
func (s *TransactionService) CreateTransaction(c *core.Context, transaction *models.Transaction, tagIds []int64) error {
if transaction.Uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -186,7 +215,24 @@ func (s *TransactionService) CreateTransaction(transaction *models.Transaction,
now := time.Now().Unix()
transaction.TransactionId = s.GenerateUuid(uuid.UUID_TYPE_TRANSACTION)
needUuidCount := 1
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
needUuidCount = 2
}
uuids := s.GenerateUuids(uuid.UUID_TYPE_TRANSACTION, uint8(needUuidCount))
if len(uuids) < needUuidCount {
return errs.ErrSystemIsBusy
}
transaction.TransactionId = uuids[0]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
transaction.RelatedId = uuids[1]
}
transaction.TransactionTime = utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
transaction.CreatedUnixTime = now
@@ -196,8 +242,14 @@ func (s *TransactionService) CreateTransaction(transaction *models.Transaction,
transactionTagIndexs := make([]*models.TransactionTagIndex, len(tagIds))
for i := 0; i < len(tagIds); i++ {
tagIndexId := s.GenerateUuid(uuid.UUID_TYPE_TAG_INDEX)
if tagIndexId < 1 {
return errs.ErrSystemIsBusy
}
transactionTagIndexs[i] = &models.TransactionTagIndex{
TagIndexId: s.GenerateUuid(uuid.UUID_TYPE_TAG_INDEX),
TagIndexId: tagIndexId,
Uid: transaction.Uid,
Deleted: false,
TagId: tagIds[i],
@@ -207,7 +259,7 @@ func (s *TransactionService) CreateTransaction(transaction *models.Transaction,
}
}
return s.UserDataDB(transaction.Uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(transaction.Uid).DoTransaction(c, func(sess *xorm.Session) error {
// Get and verify source and destination account
sourceAccount, destinationAccount, err := s.getAccountModels(sess, transaction)
@@ -256,8 +308,7 @@ func (s *TransactionService) CreateTransaction(transaction *models.Transaction,
var relatedTransaction *models.Transaction
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
relatedTransaction = s.GetRelatedTransferTransaction(transaction, s.GenerateUuid(uuid.UUID_TYPE_TRANSACTION))
transaction.RelatedId = relatedTransaction.TransactionId
relatedTransaction = s.GetRelatedTransferTransaction(transaction)
}
createdRows, err := sess.Insert(transaction)
@@ -372,7 +423,7 @@ func (s *TransactionService) CreateTransaction(transaction *models.Transaction,
}
// ModifyTransaction saves an existed transaction to database
func (s *TransactionService) ModifyTransaction(transaction *models.Transaction, addTagIds []int64, removeTagIds []int64) error {
func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *models.Transaction, addTagIds []int64, removeTagIds []int64) error {
if transaction.Uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -391,8 +442,14 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction,
transactionTagIndexs := make([]*models.TransactionTagIndex, len(addTagIds))
for i := 0; i < len(addTagIds); i++ {
tagIndexId := s.GenerateUuid(uuid.UUID_TYPE_TAG_INDEX)
if tagIndexId < 1 {
return errs.ErrSystemIsBusy
}
transactionTagIndexs[i] = &models.TransactionTagIndex{
TagIndexId: s.GenerateUuid(uuid.UUID_TYPE_TAG_INDEX),
TagIndexId: tagIndexId,
Uid: transaction.Uid,
Deleted: false,
TagId: addTagIds[i],
@@ -402,7 +459,7 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction,
}
}
err := s.UserDataDB(transaction.Uid).DoTransaction(func(sess *xorm.Session) error {
err := s.UserDataDB(transaction.Uid).DoTransaction(c, func(sess *xorm.Session) error {
// Get and verify current transaction
oldTransaction := &models.Transaction{}
has, err := sess.ID(transaction.TransactionId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(oldTransaction)
@@ -520,6 +577,14 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction,
updateCols = append(updateCols, "comment")
}
if transaction.GeoLongitude != oldTransaction.GeoLongitude {
updateCols = append(updateCols, "geo_longitude")
}
if transaction.GeoLatitude != oldTransaction.GeoLatitude {
updateCols = append(updateCols, "geo_latitude")
}
// Get and verify tags
err = s.isTagsValid(sess, transaction, transactionTagIndexs, addTagIds)
@@ -537,7 +602,7 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction,
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
relatedTransaction := s.GetRelatedTransferTransaction(transaction, transaction.RelatedId)
relatedTransaction := s.GetRelatedTransferTransaction(transaction)
if utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) != utils.GetUnixTimeFromTransactionTime(relatedTransaction.TransactionTime) {
return errs.ErrTooMuchTransactionInOneSecond
@@ -735,7 +800,7 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction,
}
// DeleteTransaction deletes an existed transaction from database
func (s *TransactionService) DeleteTransaction(uid int64, transactionId int64) error {
func (s *TransactionService) DeleteTransaction(c *core.Context, uid int64, transactionId int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -752,7 +817,7 @@ func (s *TransactionService) DeleteTransaction(uid int64, transactionId int64) e
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
// Get and verify current transaction
oldTransaction := &models.Transaction{}
has, err := sess.ID(transactionId).Where("uid=? AND deleted=?", uid, false).Get(oldTransaction)
@@ -855,7 +920,7 @@ func (s *TransactionService) DeleteTransaction(uid int64, transactionId int64) e
}
// DeleteAllTransactions deletes all existed transactions from database
func (s *TransactionService) DeleteAllTransactions(uid int64) error {
func (s *TransactionService) DeleteAllTransactions(c *core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -878,7 +943,7 @@ func (s *TransactionService) DeleteAllTransactions(uid int64) error {
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error {
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
// Update all transaction to deleted
_, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
@@ -905,7 +970,7 @@ func (s *TransactionService) DeleteAllTransactions(uid int64) error {
}
// GetRelatedTransferTransaction returns the related transaction for transfer transaction
func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *models.Transaction, relatedTransactionId int64) *models.Transaction {
func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *models.Transaction) *models.Transaction {
var relatedType models.TransactionDbType
var relatedTransactionTime int64
@@ -920,7 +985,7 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *
}
relatedTransaction := &models.Transaction{
TransactionId: relatedTransactionId,
TransactionId: originalTransaction.RelatedId,
Uid: originalTransaction.Uid,
Deleted: originalTransaction.Deleted,
Type: relatedType,
@@ -933,6 +998,9 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *
RelatedAccountId: originalTransaction.AccountId,
RelatedAccountAmount: originalTransaction.Amount,
Comment: originalTransaction.Comment,
GeoLongitude: originalTransaction.GeoLongitude,
GeoLatitude: originalTransaction.GeoLatitude,
CreatedIp: originalTransaction.CreatedIp,
CreatedUnixTime: originalTransaction.CreatedUnixTime,
UpdatedUnixTime: originalTransaction.UpdatedUnixTime,
DeletedUnixTime: originalTransaction.DeletedUnixTime,
@@ -942,7 +1010,7 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *
}
// GetAccountsTotalIncomeAndExpense returns the every accounts total income and expense amount by specific date range
func (s *TransactionService) GetAccountsTotalIncomeAndExpense(uid int64, startUnixTime int64, endUnixTime int64) (map[int64]int64, map[int64]int64, error) {
func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) (map[int64]int64, map[int64]int64, error) {
if uid <= 0 {
return nil, nil, errs.ErrUserIdInvalid
}
@@ -951,7 +1019,7 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(uid int64, startUn
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
var transactionTotalAmounts []*models.Transaction
err := s.UserDataDB(uid).Select("uid, type, account_id, SUM(amount) as amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, startTransactionTime, endTransactionTime).GroupBy("type, account_id").Find(&transactionTotalAmounts)
err := s.UserDataDB(uid).NewSession(c).Select("type, account_id, SUM(amount) as amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, startTransactionTime, endTransactionTime).GroupBy("type, account_id").Find(&transactionTotalAmounts)
if err != nil {
return nil, nil, err
@@ -974,7 +1042,7 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(uid int64, startUn
}
// GetAccountsMonthTotalIncomeAndExpense returns the every accounts total income and expense amount in month by specific date range
func (s *TransactionService) GetAccountsMonthTotalIncomeAndExpense(uid int64, startUnixTime int64, endUnixTime int64, pageCount int) (map[string]models.TransactionAccountsAmount, error) {
func (s *TransactionService) GetAccountsMonthTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, pageCount int) (map[string]models.TransactionAccountsAmount, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -989,7 +1057,7 @@ func (s *TransactionService) GetAccountsMonthTotalIncomeAndExpense(uid int64, st
for maxTransactionTime > 0 {
var transactions []*models.Transaction
err := s.UserDataDB(uid).Select("uid, type, account_id, transaction_time, timezone_utc_offset, amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, minTransactionTime, maxTransactionTime).Limit(pageCount, 0).OrderBy("transaction_time desc").Find(&transactions)
err := s.UserDataDB(uid).NewSession(c).Select("uid, type, account_id, transaction_time, timezone_utc_offset, amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, minTransactionTime, maxTransactionTime).Limit(pageCount, 0).OrderBy("transaction_time desc").Find(&transactions)
if err != nil {
return nil, err
@@ -1041,13 +1109,13 @@ func (s *TransactionService) GetAccountsMonthTotalIncomeAndExpense(uid int64, st
}
// GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(uid int64, startUnixTime int64, endUnixTime int64) ([]*models.Transaction, error) {
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
conditionParams := make([]interface{}, 0, 8)
conditionParams := make([]any, 0, 8)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
@@ -1064,7 +1132,7 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(uid i
}
var transactionTotalAmounts []*models.Transaction
err := s.UserDataDB(uid).Select("uid, category_id, account_id, SUM(amount) as amount").Where(condition, conditionParams...).GroupBy("category_id, account_id").Find(&transactionTotalAmounts)
err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, SUM(amount) as amount").Where(condition, conditionParams...).GroupBy("category_id, account_id").Find(&transactionTotalAmounts)
if err != nil {
return nil, err
@@ -1085,9 +1153,9 @@ func (s *TransactionService) GetTransactionMapByList(transactions []*models.Tran
return transactionMap
}
func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountId int64, keyword string, noDuplicated bool) (string, []interface{}) {
func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, noDuplicated bool) (string, []any) {
condition := "uid=? AND deleted=?"
conditionParams := make([]interface{}, 0, 16)
conditionParams := make([]any, 0, 16)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
@@ -1105,7 +1173,7 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
condition = condition + " AND type=?"
conditionParams = append(conditionParams, transactionType)
} else if transactionType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionType == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
if accountId == 0 {
if len(accountIds) == 0 {
condition = condition + " AND type=?"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
} else {
@@ -1114,7 +1182,7 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN)
}
} else {
if noDuplicated && accountId == 0 {
if noDuplicated && len(accountIds) == 0 {
condition = condition + " AND (type=? OR type=? OR type=? OR type=?)"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
@@ -1138,9 +1206,19 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
condition = condition + " AND category_id IN (" + conditions.String() + ")"
}
if accountId > 0 {
condition = condition + " AND account_id=?"
conditionParams = append(conditionParams, accountId)
if len(accountIds) > 0 {
var conditions strings.Builder
for i := 0; i < len(accountIds); i++ {
if i > 0 {
conditions.WriteString(",")
}
conditions.WriteString("?")
conditionParams = append(conditionParams, accountIds[i])
}
condition = condition + " AND account_id IN (" + conditions.String() + ")"
}
if keyword != "" {
+18 -17
View File
@@ -7,6 +7,7 @@ import (
"github.com/pquerna/otp/totp"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -45,13 +46,13 @@ var (
)
// GetUserTwoFactorSettingByUid returns the 2fa setting model according to user uid
func (s *TwoFactorAuthorizationService) GetUserTwoFactorSettingByUid(uid int64) (*models.TwoFactor, error) {
func (s *TwoFactorAuthorizationService) GetUserTwoFactorSettingByUid(c *core.Context, uid int64) (*models.TwoFactor, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
twoFactor := &models.TwoFactor{}
has, err := s.UserDB().Where("uid=?", uid).Get(twoFactor)
has, err := s.UserDB().NewSession(c).Where("uid=?", uid).Get(twoFactor)
if err != nil {
return nil, err
@@ -69,7 +70,7 @@ func (s *TwoFactorAuthorizationService) GetUserTwoFactorSettingByUid(uid int64)
}
// GenerateTwoFactorSecret generates a new 2fa secret
func (s *TwoFactorAuthorizationService) GenerateTwoFactorSecret(user *models.User) (*otp.Key, error) {
func (s *TwoFactorAuthorizationService) GenerateTwoFactorSecret(c *core.Context, user *models.User) (*otp.Key, error) {
if user == nil {
return nil, errs.ErrUserNotFound
}
@@ -85,7 +86,7 @@ func (s *TwoFactorAuthorizationService) GenerateTwoFactorSecret(user *models.Use
}
// CreateTwoFactorSetting saves a new 2fa setting to database
func (s *TwoFactorAuthorizationService) CreateTwoFactorSetting(twoFactor *models.TwoFactor) error {
func (s *TwoFactorAuthorizationService) CreateTwoFactorSetting(c *core.Context, twoFactor *models.TwoFactor) error {
if twoFactor.Uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -99,19 +100,19 @@ func (s *TwoFactorAuthorizationService) CreateTwoFactorSetting(twoFactor *models
twoFactor.CreatedUnixTime = time.Now().Unix()
return s.UserDB().DoTransaction(func(sess *xorm.Session) error {
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Insert(twoFactor)
return err
})
}
// DeleteTwoFactorSetting deletes an existed 2fa setting from database
func (s *TwoFactorAuthorizationService) DeleteTwoFactorSetting(uid int64) error {
func (s *TwoFactorAuthorizationService) DeleteTwoFactorSetting(c *core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.UserDB().DoTransaction(func(sess *xorm.Session) error {
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
deletedRows, err := sess.Where("uid=?", uid).Delete(&models.TwoFactor{})
if err != nil {
@@ -125,22 +126,22 @@ func (s *TwoFactorAuthorizationService) DeleteTwoFactorSetting(uid int64) error
}
// ExistsTwoFactorSetting returns whether the given user has existed 2fa setting
func (s *TwoFactorAuthorizationService) ExistsTwoFactorSetting(uid int64) (bool, error) {
func (s *TwoFactorAuthorizationService) ExistsTwoFactorSetting(c *core.Context, uid int64) (bool, error) {
if uid <= 0 {
return false, errs.ErrUserIdInvalid
}
return s.UserDB().Cols("uid").Where("uid=?", uid).Exist(&models.TwoFactor{})
return s.UserDB().NewSession(c).Cols("uid").Where("uid=?", uid).Exist(&models.TwoFactor{})
}
// GetAndUseUserTwoFactorRecoveryCode checks whether the given 2fa recovery code exists and marks it used
func (s *TwoFactorAuthorizationService) GetAndUseUserTwoFactorRecoveryCode(uid int64, recoveryCode string, salt string) error {
func (s *TwoFactorAuthorizationService) GetAndUseUserTwoFactorRecoveryCode(c *core.Context, uid int64, recoveryCode string, salt string) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
recoveryCode = utils.EncodePassword(recoveryCode, salt)
exists, err := s.UserDB().Cols("uid", "recovery_code").Where("uid=? AND recovery_code=? AND used=?", uid, recoveryCode, false).Exist(&models.TwoFactorRecoveryCode{})
exists, err := s.UserDB().NewSession(c).Cols("uid", "recovery_code").Where("uid=? AND recovery_code=? AND used=?", uid, recoveryCode, false).Exist(&models.TwoFactorRecoveryCode{})
if err != nil {
return err
@@ -148,7 +149,7 @@ func (s *TwoFactorAuthorizationService) GetAndUseUserTwoFactorRecoveryCode(uid i
return errs.ErrTwoFactorRecoveryCodeNotExist
}
return s.UserDB().DoTransaction(func(sess *xorm.Session) error {
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Cols("used", "used_unix_time").Where("uid=? AND recovery_code=?", uid, recoveryCode).Update(&models.TwoFactorRecoveryCode{Used: true, UsedUnixTime: time.Now().Unix()})
return err
})
@@ -159,7 +160,7 @@ func (s *TwoFactorAuthorizationService) GenerateTwoFactorRecoveryCodes() ([]stri
recoveryCodes := make([]string, twoFactorRecoveryCodeCount)
for i := 0; i < twoFactorRecoveryCodeCount; i++ {
recoveryCode, err := utils.GetRandomNumberOrLetter(twoFactorRecoveryCodeLength)
recoveryCode, err := utils.GetRandomNumberOrLowercaseLetter(twoFactorRecoveryCodeLength)
if err != nil {
return nil, err
@@ -172,7 +173,7 @@ func (s *TwoFactorAuthorizationService) GenerateTwoFactorRecoveryCodes() ([]stri
}
// CreateTwoFactorRecoveryCodes saves new 2fa recovery codes to database
func (s *TwoFactorAuthorizationService) CreateTwoFactorRecoveryCodes(uid int64, recoveryCodes []string, salt string) error {
func (s *TwoFactorAuthorizationService) CreateTwoFactorRecoveryCodes(c *core.Context, uid int64, recoveryCodes []string, salt string) error {
twoFactorRecoveryCodes := make([]*models.TwoFactorRecoveryCode, len(recoveryCodes))
for i := 0; i < len(recoveryCodes); i++ {
@@ -184,7 +185,7 @@ func (s *TwoFactorAuthorizationService) CreateTwoFactorRecoveryCodes(uid int64,
}
}
return s.UserDB().DoTransaction(func(sess *xorm.Session) error {
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Where("uid=?", uid).Delete(&models.TwoFactorRecoveryCode{})
if err != nil {
@@ -205,12 +206,12 @@ func (s *TwoFactorAuthorizationService) CreateTwoFactorRecoveryCodes(uid int64,
}
// DeleteTwoFactorRecoveryCodes deletes existed 2fa recovery codes from database
func (s *TwoFactorAuthorizationService) DeleteTwoFactorRecoveryCodes(uid int64) error {
func (s *TwoFactorAuthorizationService) DeleteTwoFactorRecoveryCodes(c *core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.UserDB().DoTransaction(func(sess *xorm.Session) error {
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Where("uid=?", uid).Delete(&models.TwoFactorRecoveryCode{})
return err
})
+227 -31
View File
@@ -1,20 +1,32 @@
package services
import (
"bytes"
"fmt"
"net/url"
"time"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/templates"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
const verifyEmailUrlFormat = "%sdesktop/#/verify_email?token=%s"
// UserService represents user service
type UserService struct {
ServiceUsingDB
ServiceUsingConfig
ServiceUsingMailer
ServiceUsingUuid
}
@@ -24,6 +36,12 @@ var (
ServiceUsingDB: ServiceUsingDB{
container: datastore.Container,
},
ServiceUsingConfig: ServiceUsingConfig{
container: settings.Container,
},
ServiceUsingMailer: ServiceUsingMailer{
container: mail.Container,
},
ServiceUsingUuid: ServiceUsingUuid{
container: uuid.Container,
},
@@ -31,14 +49,14 @@ var (
)
// GetUserByUsernameOrEmailAndPassword returns the user model according to login name and password
func (s *UserService) GetUserByUsernameOrEmailAndPassword(loginname string, password string) (*models.User, error) {
func (s *UserService) GetUserByUsernameOrEmailAndPassword(c *core.Context, loginname string, password string) (*models.User, error) {
var user *models.User
var err error
if utils.IsValidUsername(loginname) {
user, err = s.GetUserByUsername(loginname)
user, err = s.GetUserByUsername(c, loginname)
} else if utils.IsValidEmail(loginname) {
user, err = s.GetUserByEmail(loginname)
user, err = s.GetUserByEmail(c, loginname)
} else {
err = errs.ErrLoginNameInvalid
}
@@ -55,13 +73,13 @@ func (s *UserService) GetUserByUsernameOrEmailAndPassword(loginname string, pass
}
// GetUserById returns the user model according to user uid
func (s *UserService) GetUserById(uid int64) (*models.User, error) {
func (s *UserService) GetUserById(c *core.Context, uid int64) (*models.User, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
user := &models.User{}
has, err := s.UserDB().ID(uid).Where("deleted=?", false).Get(user)
has, err := s.UserDB().NewSession(c).ID(uid).Where("deleted=?", false).Get(user)
if err != nil {
return nil, err
@@ -73,13 +91,13 @@ func (s *UserService) GetUserById(uid int64) (*models.User, error) {
}
// GetUserByUsername returns the user model according to user name
func (s *UserService) GetUserByUsername(username string) (*models.User, error) {
func (s *UserService) GetUserByUsername(c *core.Context, username string) (*models.User, error) {
if username == "" {
return nil, errs.ErrUsernameIsEmpty
}
user := &models.User{}
has, err := s.UserDB().Where("username=? AND deleted=?", username, false).Get(user)
has, err := s.UserDB().NewSession(c).Where("username=? AND deleted=?", username, false).Get(user)
if err != nil {
return nil, err
@@ -91,13 +109,13 @@ func (s *UserService) GetUserByUsername(username string) (*models.User, error) {
}
// GetUserByEmail returns the user model according to user email
func (s *UserService) GetUserByEmail(email string) (*models.User, error) {
func (s *UserService) GetUserByEmail(c *core.Context, email string) (*models.User, error) {
if email == "" {
return nil, errs.ErrEmailIsEmpty
}
user := &models.User{}
has, err := s.UserDB().Where("email=? AND deleted=?", email, false).Get(user)
has, err := s.UserDB().NewSession(c).Where("email=? AND deleted=?", email, false).Get(user)
if err != nil {
return nil, err
@@ -109,8 +127,8 @@ func (s *UserService) GetUserByEmail(email string) (*models.User, error) {
}
// CreateUser saves a new user model to database
func (s *UserService) CreateUser(user *models.User) error {
exists, err := s.ExistsUsername(user.Username)
func (s *UserService) CreateUser(c *core.Context, user *models.User) error {
exists, err := s.ExistsUsername(c, user.Username)
if err != nil {
return err
@@ -118,7 +136,7 @@ func (s *UserService) CreateUser(user *models.User) error {
return errs.ErrUsernameAlreadyExists
}
exists, err = s.ExistsEmail(user.Email)
exists, err = s.ExistsEmail(c, user.Email)
if err != nil {
return err
@@ -135,6 +153,11 @@ func (s *UserService) CreateUser(user *models.User) error {
}
user.Uid = s.GenerateUuid(uuid.UUID_TYPE_USER)
if user.Uid < 1 {
return errs.ErrSystemIsBusy
}
user.Password = utils.EncodePassword(user.Password, user.Salt)
user.Deleted = false
@@ -143,36 +166,39 @@ func (s *UserService) CreateUser(user *models.User) error {
user.UpdatedUnixTime = time.Now().Unix()
user.LastLoginUnixTime = time.Now().Unix()
return s.UserDB().DoTransaction(func(sess *xorm.Session) error {
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Insert(user)
return err
})
}
// UpdateUser saves an existed user model to database
func (s *UserService) UpdateUser(user *models.User) (keyProfileUpdated bool, err error) {
func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserLanguage bool) (keyProfileUpdated bool, emailSetToUnverified bool, err error) {
if user.Uid <= 0 {
return false, errs.ErrUserIdInvalid
return false, false, errs.ErrUserIdInvalid
}
updateCols := make([]string, 0, 8)
now := time.Now().Unix()
keyProfileUpdated = false
emailSetToUnverified = false
if user.Email != "" {
exists, err := s.ExistsEmail(user.Email)
exists, err := s.ExistsEmail(c, user.Email)
if err != nil {
return false, err
return false, false, err
} else if exists {
return false, errs.ErrUserEmailAlreadyExists
return false, false, errs.ErrUserEmailAlreadyExists
}
user.EmailVerified = false
updateCols = append(updateCols, "email")
updateCols = append(updateCols, "email_verified")
emailSetToUnverified = true
}
if user.Password != "" {
@@ -186,6 +212,18 @@ func (s *UserService) UpdateUser(user *models.User) (keyProfileUpdated bool, err
updateCols = append(updateCols, "nickname")
}
if user.DefaultAccountId > 0 {
updateCols = append(updateCols, "default_account_id")
}
if models.TRANSACTION_EDIT_SCOPE_NONE <= user.TransactionEditScope && user.TransactionEditScope <= models.TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER {
updateCols = append(updateCols, "transaction_edit_scope")
}
if modifyUserLanguage || user.Language != "" {
updateCols = append(updateCols, "language")
}
if user.DefaultCurrency != "" {
updateCols = append(updateCols, "default_currency")
}
@@ -194,14 +232,26 @@ func (s *UserService) UpdateUser(user *models.User) (keyProfileUpdated bool, err
updateCols = append(updateCols, "first_day_of_week")
}
if models.TRANSACTION_EDIT_SCOPE_NONE <= user.TransactionEditScope && user.TransactionEditScope <= models.TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER {
updateCols = append(updateCols, "transaction_edit_scope")
if models.LONG_DATE_FORMAT_DEFAULT <= user.LongDateFormat && user.LongDateFormat <= models.LONG_DATE_FORMAT_D_M_YYYY {
updateCols = append(updateCols, "long_date_format")
}
if models.SHORT_DATE_FORMAT_DEFAULT <= user.ShortDateFormat && user.ShortDateFormat <= models.SHORT_DATE_FORMAT_D_M_YYYY {
updateCols = append(updateCols, "short_date_format")
}
if models.LONG_TIME_FORMAT_DEFAULT <= user.LongTimeFormat && user.LongTimeFormat <= models.LONG_TIME_FORMAT_HH_MM_SS_A {
updateCols = append(updateCols, "long_time_format")
}
if models.SHORT_TIME_FORMAT_DEFAULT <= user.ShortTimeFormat && user.ShortTimeFormat <= models.SHORT_TIME_FORMAT_HH_MM_A {
updateCols = append(updateCols, "short_time_format")
}
user.UpdatedUnixTime = now
updateCols = append(updateCols, "updated_unix_time")
err = s.UserDB().DoTransaction(func(sess *xorm.Session) error {
err = s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.ID(user.Uid).Cols(updateCols...).Where("deleted=?", false).Update(user)
if err != nil {
@@ -214,26 +264,118 @@ func (s *UserService) UpdateUser(user *models.User) (keyProfileUpdated bool, err
})
if err != nil {
return false, err
return false, false, err
}
return keyProfileUpdated, nil
return keyProfileUpdated, emailSetToUnverified, nil
}
// UpdateUserLastLoginTime updates the last login time field
func (s *UserService) UpdateUserLastLoginTime(uid int64) error {
func (s *UserService) UpdateUserLastLoginTime(c *core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.UserDB().DoTransaction(func(sess *xorm.Session) error {
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.ID(uid).Cols("last_login_unix_time").Where("deleted=?", false).Update(&models.User{LastLoginUnixTime: time.Now().Unix()})
return err
})
}
// EnableUser sets user enabled
func (s *UserService) EnableUser(c *core.Context, username string) error {
if username == "" {
return errs.ErrUsernameIsEmpty
}
now := time.Now().Unix()
updateModel := &models.User{
Disabled: false,
UpdatedUnixTime: now,
}
updatedRows, err := s.UserDB().NewSession(c).Cols("disabled", "updated_unix_time").Where("username=? AND deleted=?", username, false).Update(updateModel)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrUserNotFound
}
return nil
}
// DisableUser sets user disabled
func (s *UserService) DisableUser(c *core.Context, username string) error {
if username == "" {
return errs.ErrUsernameIsEmpty
}
now := time.Now().Unix()
updateModel := &models.User{
Disabled: true,
UpdatedUnixTime: now,
}
updatedRows, err := s.UserDB().NewSession(c).Cols("disabled", "updated_unix_time").Where("username=? AND deleted=?", username, false).Update(updateModel)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrUserNotFound
}
return nil
}
// SetUserEmailVerified sets user email address verified
func (s *UserService) SetUserEmailVerified(c *core.Context, username string) error {
if username == "" {
return errs.ErrUsernameIsEmpty
}
now := time.Now().Unix()
updateModel := &models.User{
EmailVerified: true,
UpdatedUnixTime: now,
}
updatedRows, err := s.UserDB().NewSession(c).Cols("email_verified", "updated_unix_time").Where("username=? AND deleted=?", username, false).Update(updateModel)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrUserNotFound
}
return nil
}
// SetUserEmailUnverified sets user email address unverified
func (s *UserService) SetUserEmailUnverified(c *core.Context, username string) error {
if username == "" {
return errs.ErrUsernameIsEmpty
}
now := time.Now().Unix()
updateModel := &models.User{
EmailVerified: false,
UpdatedUnixTime: now,
}
updatedRows, err := s.UserDB().NewSession(c).Cols("email_verified", "updated_unix_time").Where("username=? AND deleted=?", username, false).Update(updateModel)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrUserNotFound
}
return nil
}
// DeleteUser deletes an existed user from database
func (s *UserService) DeleteUser(username string) error {
func (s *UserService) DeleteUser(c *core.Context, username string) error {
if username == "" {
return errs.ErrUsernameIsEmpty
}
@@ -245,7 +387,7 @@ func (s *UserService) DeleteUser(username string) error {
DeletedUnixTime: now,
}
deletedRows, err := s.UserDB().Cols("deleted", "deleted_unix_time").Where("username=? AND deleted=?", username, false).Update(updateModel)
deletedRows, err := s.UserDB().NewSession(c).Cols("deleted", "deleted_unix_time").Where("username=? AND deleted=?", username, false).Update(updateModel)
if err != nil {
return err
@@ -256,21 +398,75 @@ func (s *UserService) DeleteUser(username string) error {
}
// ExistsUsername returns whether the given user name exists
func (s *UserService) ExistsUsername(username string) (bool, error) {
func (s *UserService) ExistsUsername(c *core.Context, username string) (bool, error) {
if username == "" {
return false, errs.ErrUsernameIsEmpty
}
return s.UserDB().Cols("username").Where("username=? AND deleted=?", username, false).Exist(&models.User{})
return s.UserDB().NewSession(c).Cols("username").Where("username=? AND deleted=?", username, false).Exist(&models.User{})
}
// ExistsEmail returns whether the given user email exists
func (s *UserService) ExistsEmail(email string) (bool, error) {
func (s *UserService) ExistsEmail(c *core.Context, email string) (bool, error) {
if email == "" {
return false, errs.ErrEmailIsEmpty
}
return s.UserDB().Cols("email").Where("email=? AND deleted=?", email, false).Exist(&models.User{})
return s.UserDB().NewSession(c).Cols("email").Where("email=? AND deleted=?", email, false).Exist(&models.User{})
}
// SendVerifyEmail sends verify email according to specified parameters
func (s *UserService) SendVerifyEmail(user *models.User, verifyEmailToken string, backupLocale string) error {
if !s.CurrentConfig().EnableSMTP {
return errs.ErrSMTPServerNotEnabled
}
locale := user.Language
if locale == "" {
locale = backupLocale
}
localeTextItems := locales.GetLocaleTextItems(locale)
verifyEmailTextItems := localeTextItems.VerifyEmailTextItems
expireTimeInMinutes := s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration.Minutes()
verifyEmailUrl := fmt.Sprintf(verifyEmailUrlFormat, s.CurrentConfig().RootUrl, url.QueryEscape(verifyEmailToken))
tmpl, err := templates.GetTemplate(templates.TEMPLATE_VERIFY_EMAIL)
if err != nil {
return err
}
templateParams := map[string]any{
"AppName": s.CurrentConfig().AppName,
"VerifyEmail": map[string]any{
"Title": verifyEmailTextItems.Title,
"Salutation": fmt.Sprintf(verifyEmailTextItems.SalutationFormat, user.Nickname),
"DescriptionAboveBtn": verifyEmailTextItems.DescriptionAboveBtn,
"VerifyEmailUrl": verifyEmailUrl,
"VerifyEmail": verifyEmailTextItems.VerifyEmail,
"DescriptionBelowBtn": fmt.Sprintf(verifyEmailTextItems.DescriptionBelowBtnFormat, s.CurrentConfig().AppName, expireTimeInMinutes),
},
}
var bodyBuffer bytes.Buffer
err = tmpl.Execute(&bodyBuffer, templateParams)
if err != nil {
return err
}
message := &mail.MailMessage{
To: user.Email,
Subject: verifyEmailTextItems.Title,
Body: bodyBuffer.String(),
}
err = s.SendMail(message)
return err
}
// IsPasswordEqualsUserPassword returns whether the given password is correct
+257 -46
View File
@@ -62,36 +62,64 @@ const (
InternalUuidGeneratorType string = "internal"
)
// User avatar provider types
const (
GravatarProvider string = "gravatar"
)
// Map provider types
const (
OpenStreetMapProvider string = "openstreetmap"
OpenStreetMapHumanitarianStyleProvider string = "openstreetmap_humanitarian"
OpenTopoMapProvider string = "opentopomap"
OPNVKarteMapProvider string = "opnvkarte"
CyclOSMMapProvider string = "cyclosm"
GoogleMapProvider string = "googlemap"
TomTomMapProvider string = "tomtom"
BaiduMapProvider string = "baidumap"
AmapProvider string = "amap"
)
// Amap security verification method
const (
AmapSecurityVerificationInternalProxyMethod string = "internal_proxy"
AmapSecurityVerificationExternalProxyMethod string = "external_proxy"
AmapSecurityVerificationPlainTextMethod string = "plain_text"
)
// Exchange rates data source types
const (
EuroCentralBankDataSource string = "euro_central_bank"
BankOfCanadaDataSource string = "bank_of_canada"
ReserveBankOfAustraliaDataSource string = "reserve_bank_of_australia"
CzechNationalBankDataSource string = "czech_national_bank"
NationalBankOfPolandDataSource string = "national_bank_of_poland"
EuroCentralBankDataSource string = "euro_central_bank"
BankOfCanadaDataSource string = "bank_of_canada"
ReserveBankOfAustraliaDataSource string = "reserve_bank_of_australia"
CzechNationalBankDataSource string = "czech_national_bank"
NationalBankOfPolandDataSource string = "national_bank_of_poland"
MonetaryAuthorityOfSingaporeDataSource string = "monetary_authority_of_singapore"
)
const (
defaultAppName string = "ezBookkeeping"
defaultHttpAddr string = "0.0.0.0"
defaultHttpPort int = 8080
defaultHttpPort uint16 = 8080
defaultDomain string = "localhost"
defaultDatabaseHost string = "127.0.0.1:3306"
defaultDatabaseName string = "ezbookkeeping"
defaultDatabaseMaxIdleConn int = 2
defaultDatabaseMaxOpenConn int = 0
defaultDatabaseConnMaxLifetime int = 14400
defaultDatabaseMaxIdleConn uint16 = 2
defaultDatabaseMaxOpenConn uint16 = 0
defaultDatabaseConnMaxLifetime uint32 = 14400
defaultLogMode string = "console"
defaultLoglevel Level = LOGLEVEL_INFO
defaultSecretKey string = "ezbookkeeping"
defaultTokenExpiredTime int = 604800 // 7 days
defaultTemporaryTokenExpiredTime int = 300 // 5 minutes
defaultSecretKey string = "ezbookkeeping"
defaultTokenExpiredTime uint32 = 604800 // 7 days
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes
defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes
defaultExchangeRatesDataRequestTimeout int = 10000 // 10 seconds
defaultExchangeRatesDataRequestTimeout uint32 = 10000 // 10 seconds
)
// DatabaseConfig represents the database setting config
@@ -106,9 +134,18 @@ type DatabaseConfig struct {
DatabasePath string
MaxIdleConnection int
MaxOpenConnection int
ConnectionMaxLifeTime int
MaxIdleConnection uint16
MaxOpenConnection uint16
ConnectionMaxLifeTime uint32
}
// SMTPConfig represents the SMTP setting config
type SMTPConfig struct {
SMTPHost string
SMTPUser string
SMTPPasswd string
SMTPSkipTLSVerify bool
FromAddress string
}
// Config represents the global setting config
@@ -121,7 +158,7 @@ type Config struct {
// Server
Protocol Scheme
HttpAddr string
HttpPort int
HttpPort uint16
Domain string
RootUrl string
@@ -141,6 +178,10 @@ type Config struct {
EnableQueryLog bool
AutoUpdateDatabase bool
// Mail
EnableSMTP bool
SMTPConfig *SMTPConfig
// Log
LogModes []string
EnableConsoleLog bool
@@ -154,23 +195,44 @@ type Config struct {
UuidServerId uint8
// Secret
SecretKey string
EnableTwoFactor bool
TokenExpiredTime int
TokenExpiredTimeDuration time.Duration
TemporaryTokenExpiredTime int
TemporaryTokenExpiredTimeDuration time.Duration
EnableRequestIdHeader bool
SecretKey string
EnableTwoFactor bool
TokenExpiredTime uint32
TokenExpiredTimeDuration time.Duration
TemporaryTokenExpiredTime uint32
TemporaryTokenExpiredTimeDuration time.Duration
EmailVerifyTokenExpiredTime uint32
EmailVerifyTokenExpiredTimeDuration time.Duration
PasswordResetTokenExpiredTime uint32
PasswordResetTokenExpiredTimeDuration time.Duration
EnableRequestIdHeader bool
// User
EnableUserRegister bool
EnableUserRegister bool
EnableUserVerifyEmail bool
EnableUserForceVerifyEmail bool
EnableUserForgetPassword bool
ForgetPasswordRequireVerifyEmail bool
AvatarProvider string
// Data
EnableDataExport bool
// Map
MapProvider string
TomTomMapAPIKey string
GoogleMapAPIKey string
BaiduMapAK string
AmapApplicationKey string
AmapSecurityVerificationMethod string
AmapApplicationSecret string
AmapApiExternalProxyUrl string
EnableMapDataFetchProxy bool
// Exchange Rates
ExchangeRatesDataSource string
ExchangeRatesRequestTimeout int
ExchangeRatesRequestTimeout uint32
ExchangeRatesSkipTLSVerify bool
}
// LoadConfiguration loads setting config from given config file path
@@ -208,6 +270,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
return nil, err
}
err = loadMailConfiguration(config, cfgFile, "mail")
if err != nil {
return nil, err
}
err = loadLogConfiguration(config, cfgFile, "log")
if err != nil {
@@ -238,6 +306,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
return nil, err
}
err = loadMapConfiguration(config, cfgFile, "map")
if err != nil {
return nil, err
}
err = loadExchangeRatesConfiguration(config, cfgFile, "exchange_rates")
if err != nil {
@@ -281,12 +355,12 @@ func loadServerConfiguration(config *Config, configFile *ini.File, sectionName s
config.Protocol = SCHEME_HTTP
config.HttpAddr = getConfigItemStringValue(configFile, sectionName, "http_addr", defaultHttpAddr)
config.HttpPort = getConfigItemIntValue(configFile, sectionName, "http_port", defaultHttpPort)
config.HttpPort = getConfigItemUint16Value(configFile, sectionName, "http_port", defaultHttpPort)
} else if getConfigItemStringValue(configFile, sectionName, "protocol") == "https" {
config.Protocol = SCHEME_HTTPS
config.HttpAddr = getConfigItemStringValue(configFile, sectionName, "http_addr", defaultHttpAddr)
config.HttpPort = getConfigItemIntValue(configFile, sectionName, "http_port", defaultHttpPort)
config.HttpPort = getConfigItemUint16Value(configFile, sectionName, "http_port", defaultHttpPort)
config.CertFile = getConfigItemStringValue(configFile, sectionName, "cert_file")
config.CertKeyFile = getConfigItemStringValue(configFile, sectionName, "cert_key_file")
@@ -339,9 +413,9 @@ func loadDatabaseConfiguration(config *Config, configFile *ini.File, sectionName
dbConfig.DatabasePath = finalStaticDBPath
}
dbConfig.MaxIdleConnection = getConfigItemIntValue(configFile, sectionName, "max_idle_conn", defaultDatabaseMaxIdleConn)
dbConfig.MaxOpenConnection = getConfigItemIntValue(configFile, sectionName, "max_open_conn", defaultDatabaseMaxOpenConn)
dbConfig.ConnectionMaxLifeTime = getConfigItemIntValue(configFile, sectionName, "conn_max_lifetime", defaultDatabaseConnMaxLifetime)
dbConfig.MaxIdleConnection = getConfigItemUint16Value(configFile, sectionName, "max_idle_conn", defaultDatabaseMaxIdleConn)
dbConfig.MaxOpenConnection = getConfigItemUint16Value(configFile, sectionName, "max_open_conn", defaultDatabaseMaxOpenConn)
dbConfig.ConnectionMaxLifeTime = getConfigItemUint32Value(configFile, sectionName, "conn_max_lifetime", defaultDatabaseConnMaxLifetime)
config.DatabaseConfig = dbConfig
config.EnableQueryLog = getConfigItemBoolValue(configFile, sectionName, "log_query", false)
@@ -350,6 +424,22 @@ func loadDatabaseConfiguration(config *Config, configFile *ini.File, sectionName
return nil
}
func loadMailConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.EnableSMTP = getConfigItemBoolValue(configFile, sectionName, "enable_smtp", false)
smtpConfig := &SMTPConfig{}
smtpConfig.SMTPHost = getConfigItemStringValue(configFile, sectionName, "smtp_host")
smtpConfig.SMTPUser = getConfigItemStringValue(configFile, sectionName, "smtp_user")
smtpConfig.SMTPPasswd = getConfigItemStringValue(configFile, sectionName, "smtp_passwd")
smtpConfig.SMTPSkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "smtp_skip_tls_verify", false)
smtpConfig.FromAddress = getConfigItemStringValue(configFile, sectionName, "from_address")
config.SMTPConfig = smtpConfig
return nil
}
func loadLogConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.LogModes = strings.Split(getConfigItemStringValue(configFile, sectionName, "mode", defaultLogMode), " ")
@@ -383,7 +473,7 @@ func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName str
return errs.ErrInvalidUuidMode
}
config.UuidServerId = uint8(getConfigItemIntValue(configFile, sectionName, "server_id", 0))
config.UuidServerId = getConfigItemUint8Value(configFile, sectionName, "server_id", 0)
return nil
}
@@ -392,12 +482,18 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
config.SecretKey = getConfigItemStringValue(configFile, sectionName, "secret_key", defaultSecretKey)
config.EnableTwoFactor = getConfigItemBoolValue(configFile, sectionName, "enable_two_factor", true)
config.TokenExpiredTime = getConfigItemIntValue(configFile, sectionName, "token_expired_time", defaultTokenExpiredTime)
config.TokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "token_expired_time", defaultTokenExpiredTime)
config.TokenExpiredTimeDuration = time.Duration(config.TokenExpiredTime) * time.Second
config.TemporaryTokenExpiredTime = getConfigItemIntValue(configFile, sectionName, "temporary_token_expired_time", defaultTemporaryTokenExpiredTime)
config.TemporaryTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "temporary_token_expired_time", defaultTemporaryTokenExpiredTime)
config.TemporaryTokenExpiredTimeDuration = time.Duration(config.TemporaryTokenExpiredTime) * time.Second
config.EmailVerifyTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "email_verify_token_expired_time", defaultEmailVerifyTokenExpiredTime)
config.EmailVerifyTokenExpiredTimeDuration = time.Duration(config.EmailVerifyTokenExpiredTime) * time.Second
config.PasswordResetTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "password_reset_token_expired_time", defaultPasswordResetTokenExpiredTime)
config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second
config.EnableRequestIdHeader = getConfigItemBoolValue(configFile, sectionName, "request_id_header", true)
return nil
@@ -405,6 +501,16 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
func loadUserConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.EnableUserRegister = getConfigItemBoolValue(configFile, sectionName, "enable_register", false)
config.EnableUserVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_email_verify", false)
config.EnableUserForceVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_force_email_verify", false)
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
config.AvatarProvider = ""
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == GravatarProvider {
config.AvatarProvider = GravatarProvider
}
return nil
}
@@ -415,22 +521,77 @@ func loadDataConfiguration(config *Config, configFile *ini.File, sectionName str
return nil
}
func loadMapConfiguration(config *Config, configFile *ini.File, sectionName string) error {
mapProvider := getConfigItemStringValue(configFile, sectionName, "map_provider")
if mapProvider == "" {
config.MapProvider = ""
} else if mapProvider == OpenStreetMapProvider {
config.MapProvider = OpenStreetMapProvider
} else if mapProvider == OpenStreetMapHumanitarianStyleProvider {
config.MapProvider = OpenStreetMapHumanitarianStyleProvider
} else if mapProvider == OpenTopoMapProvider {
config.MapProvider = OpenTopoMapProvider
} else if mapProvider == OPNVKarteMapProvider {
config.MapProvider = OPNVKarteMapProvider
} else if mapProvider == CyclOSMMapProvider {
config.MapProvider = CyclOSMMapProvider
} else if mapProvider == GoogleMapProvider {
config.MapProvider = GoogleMapProvider
} else if mapProvider == TomTomMapProvider {
config.MapProvider = TomTomMapProvider
} else if mapProvider == BaiduMapProvider {
config.MapProvider = BaiduMapProvider
} else if mapProvider == AmapProvider {
config.MapProvider = AmapProvider
} else {
return errs.ErrInvalidMapProvider
}
config.EnableMapDataFetchProxy = getConfigItemBoolValue(configFile, sectionName, "map_data_fetch_proxy", false)
config.TomTomMapAPIKey = getConfigItemStringValue(configFile, sectionName, "tomtom_map_api_key")
config.GoogleMapAPIKey = getConfigItemStringValue(configFile, sectionName, "google_map_api_key")
config.BaiduMapAK = getConfigItemStringValue(configFile, sectionName, "baidu_map_ak")
config.AmapApplicationKey = getConfigItemStringValue(configFile, sectionName, "amap_application_key")
amapSecurityVerificationMethod := getConfigItemStringValue(configFile, sectionName, "amap_security_verification_method")
if amapSecurityVerificationMethod == AmapSecurityVerificationInternalProxyMethod {
config.AmapSecurityVerificationMethod = AmapSecurityVerificationInternalProxyMethod
} else if amapSecurityVerificationMethod == AmapSecurityVerificationExternalProxyMethod {
config.AmapSecurityVerificationMethod = AmapSecurityVerificationExternalProxyMethod
} else if amapSecurityVerificationMethod == AmapSecurityVerificationPlainTextMethod {
config.AmapSecurityVerificationMethod = AmapSecurityVerificationPlainTextMethod
} else {
return errs.ErrInvalidAmapSecurityVerificationMethod
}
config.AmapApplicationSecret = getConfigItemStringValue(configFile, sectionName, "amap_application_secret")
config.AmapApiExternalProxyUrl = getConfigItemStringValue(configFile, sectionName, "amap_api_external_proxy_url")
return nil
}
func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "data_source") == EuroCentralBankDataSource {
dataSource := getConfigItemStringValue(configFile, sectionName, "data_source")
if dataSource == EuroCentralBankDataSource {
config.ExchangeRatesDataSource = EuroCentralBankDataSource
} else if getConfigItemStringValue(configFile, sectionName, "data_source") == BankOfCanadaDataSource {
} else if dataSource == BankOfCanadaDataSource {
config.ExchangeRatesDataSource = BankOfCanadaDataSource
} else if getConfigItemStringValue(configFile, sectionName, "data_source") == ReserveBankOfAustraliaDataSource {
} else if dataSource == ReserveBankOfAustraliaDataSource {
config.ExchangeRatesDataSource = ReserveBankOfAustraliaDataSource
} else if getConfigItemStringValue(configFile, sectionName, "data_source") == CzechNationalBankDataSource {
} else if dataSource == CzechNationalBankDataSource {
config.ExchangeRatesDataSource = CzechNationalBankDataSource
} else if getConfigItemStringValue(configFile, sectionName, "data_source") == NationalBankOfPolandDataSource {
} else if dataSource == NationalBankOfPolandDataSource {
config.ExchangeRatesDataSource = NationalBankOfPolandDataSource
} else if dataSource == MonetaryAuthorityOfSingaporeDataSource {
config.ExchangeRatesDataSource = MonetaryAuthorityOfSingaporeDataSource
} else {
return errs.ErrInvalidExchangeRatesDataSource
}
config.ExchangeRatesRequestTimeout = getConfigItemIntValue(configFile, sectionName, "request_timeout", defaultExchangeRatesDataRequestTimeout)
config.ExchangeRatesRequestTimeout = getConfigItemUint32Value(configFile, sectionName, "request_timeout", defaultExchangeRatesDataRequestTimeout)
config.ExchangeRatesSkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "skip_tls_verify", false)
return nil
}
@@ -482,23 +643,73 @@ func getConfigItemStringValue(configFile *ini.File, sectionName string, itemName
}
}
func getConfigItemIntValue(configFile *ini.File, sectionName string, itemName string, defaultValue ...int) int {
func getConfigItemUint8Value(configFile *ini.File, sectionName string, itemName string, defaultValue uint8) uint8 {
environmentKey := getEnvironmentKey(sectionName, itemName)
environmentValue := os.Getenv(environmentKey)
if len(environmentValue) > 0 {
value, err := strconv.ParseInt(environmentValue, 0, 64)
value, err := strconv.ParseUint(environmentValue, 10, 8)
if err == nil {
return int(value)
return uint8(value)
}
}
section := configFile.Section(sectionName)
return section.Key(itemName).MustInt(defaultValue...)
value, err := strconv.ParseUint(section.Key(itemName).String(), 10, 8)
if err == nil {
return uint8(value)
}
return defaultValue
}
func getConfigItemBoolValue(configFile *ini.File, sectionName string, itemName string, defaultValue ...bool) bool {
func getConfigItemUint16Value(configFile *ini.File, sectionName string, itemName string, defaultValue uint16) uint16 {
environmentKey := getEnvironmentKey(sectionName, itemName)
environmentValue := os.Getenv(environmentKey)
if len(environmentValue) > 0 {
value, err := strconv.ParseUint(environmentValue, 10, 16)
if err == nil {
return uint16(value)
}
}
section := configFile.Section(sectionName)
value, err := strconv.ParseUint(section.Key(itemName).String(), 10, 16)
if err == nil {
return uint16(value)
}
return defaultValue
}
func getConfigItemUint32Value(configFile *ini.File, sectionName string, itemName string, defaultValue uint32) uint32 {
environmentKey := getEnvironmentKey(sectionName, itemName)
environmentValue := os.Getenv(environmentKey)
if len(environmentValue) > 0 {
value, err := strconv.ParseUint(environmentValue, 10, 32)
if err == nil {
return uint32(value)
}
}
section := configFile.Section(sectionName)
value, err := strconv.ParseUint(section.Key(itemName).String(), 10, 32)
if err == nil {
return uint32(value)
}
return defaultValue
}
func getConfigItemBoolValue(configFile *ini.File, sectionName string, itemName string, defaultValue bool) bool {
environmentKey := getEnvironmentKey(sectionName, itemName)
environmentValue := os.Getenv(environmentKey)
@@ -511,7 +722,7 @@ func getConfigItemBoolValue(configFile *ini.File, sectionName string, itemName s
}
section := configFile.Section(sectionName)
return section.Key(itemName).MustBool(defaultValue...)
return section.Key(itemName).MustBool(defaultValue)
}
func getEnvironmentKey(sectionName string, itemName string) string {
+3 -1
View File
@@ -7,7 +7,9 @@ type ConfigContainer struct {
// Initialize a config container singleton instance
var (
Container = &ConfigContainer{}
Version string
CommitHash string
Container = &ConfigContainer{}
)
// SetCurrentConfig sets the current config by a given config
+9
View File
@@ -0,0 +1,9 @@
package templates
type KnownTemplate string
// Known templates
const (
TEMPLATE_VERIFY_EMAIL KnownTemplate = "email/verify_email"
TEMPLATE_PASSWORD_RESET KnownTemplate = "email/password_reset"
)
+42
View File
@@ -0,0 +1,42 @@
package templates
import (
"fmt"
"html/template"
"path/filepath"
)
const templateBasePath = "templates"
const templateFileExtension = "tmpl"
var templateCache = make(map[KnownTemplate]*CachedTemplate)
// CachedTemplate represents a cached template
type CachedTemplate struct {
templateName KnownTemplate
templateContent *template.Template
}
// GetTemplate returns a cached template instance according to the template name
func GetTemplate(templateName KnownTemplate) (*template.Template, error) {
fullPath := filepath.Join(templateBasePath, fmt.Sprintf("%s.%s", templateName, templateFileExtension))
cachedTemplate, exists := templateCache[templateName]
if exists {
return cachedTemplate.templateContent, nil
}
tmpl, err := template.ParseFiles(fullPath)
if err != nil {
return nil, err
}
templateCache[templateName] = &CachedTemplate{
templateName: templateName,
templateContent: tmpl,
}
return tmpl, err
}
+9 -3
View File
@@ -12,7 +12,7 @@ import (
)
// PrintJsonSuccessResult writes success response in json format to current http context
func PrintJsonSuccessResult(c *core.Context, result interface{}) {
func PrintJsonSuccessResult(c *core.Context, result any) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": result,
@@ -45,12 +45,18 @@ func PrintJsonErrorResult(c *core.Context, err *errs.Error) {
}
}
c.AbortWithStatusJSON(err.HttpStatusCode, gin.H{
result := gin.H{
"success": false,
"errorCode": err.Code(),
"errorMessage": errorMessage,
"path": c.Request.URL.Path,
})
}
if err.Context != nil {
result["context"] = err.Context
}
c.AbortWithStatusJSON(err.HttpStatusCode, result)
}
// PrintDataErrorResult writes error response in custom content type to current http context
+16
View File
@@ -0,0 +1,16 @@
package utils
import (
"fmt"
"strings"
)
const gravatarUrlFormat = "https://www.gravatar.com/avatar/%s"
// GetGravatarUrl returns the Gravatar url according to the specified user email address
func GetGravatarUrl(email string) string {
email = strings.TrimSpace(email)
email = strings.ToLower(email)
emailMd5 := MD5EncodeToString([]byte(email))
return fmt.Sprintf(gravatarUrlFormat, emailMd5)
}

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