Compare commits

..

441 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
322 changed files with 38416 additions and 10559 deletions
+10 -5
View File
@@ -23,28 +23,33 @@ jobs:
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
${{ secrets.CUSTOM_BACKEND_PRE_SETUP }}
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ secrets.CUSTOM_FRONTEND_PRE_SETUP }}
${{ 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
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
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 }}
labels: ${{ steps.meta.outputs.labels }}
+10 -5
View File
@@ -23,27 +23,32 @@ jobs:
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
${{ secrets.CUSTOM_BACKEND_PRE_SETUP }}
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ secrets.CUSTOM_FRONTEND_PRE_SETUP }}
${{ 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
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
+4 -3
View File
@@ -1,5 +1,5 @@
# Build backend binary file
FROM golang:1.20.4-alpine3.17 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:18.16.0-alpine3.17 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.17.3
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
+10 -5
View File
@@ -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
+3
View File
@@ -121,6 +121,9 @@ set_build_parameters() {
}
build_backend() {
echo "Pulling backend dependencies..."
go get .
if [ "$NO_LINT" = "0" ]; then
echo "Executing backend lint checking..."
go vet -v ./...
+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
+198
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)
+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())
}
}
+94 -11
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))
@@ -116,7 +121,6 @@ func startWebServer(c *cli.Context) error {
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
workboxFileNames := utils.ListFileNamesWithPrefixAndSuffix(config.StaticRootPath, "workbox-", ".js")
for i := 0; i < len(workboxFileNames); i++ {
router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
@@ -134,6 +138,11 @@ 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))
@@ -146,33 +155,73 @@ func startWebServer(c *cli.Context) error {
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
if config.EnableMapDataFetchProxy {
if config.MapProvider == settings.OpenStreetMapProvider {
proxyRoute.GET("/openstreetmap/tile/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.OpenStreetMapTileImageProxyHandler))
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))
}
apiRoute.GET("/logout.json", bindApi(api.Tokens.TokenRevokeCurrentHandler))
if config.EnableUserVerifyEmail {
apiRoute.POST("/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByUnloginUserHandler))
emailVerifyRoute := apiRoute.Group("/verify_email")
emailVerifyRoute.Use(bindMiddleware(middlewares.JWTEmailVerifyAuthorization))
{
emailVerifyRoute.POST("/by_token.json", bindApi(api.Users.UserEmailVerifyHandler))
}
}
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))
@@ -181,17 +230,21 @@ 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))
}
@@ -291,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)
@@ -304,6 +374,19 @@ 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)
+76 -2
View File
@@ -68,6 +68,19 @@ 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"
@@ -99,6 +112,12 @@ token_expired_time = 2592000
# 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,17 +125,69 @@ 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 `openstreetmap`
# 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"
# 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 the following types:
# "euro_central_bank"
@@ -129,3 +200,6 @@ data_source = euro_central_bank
# 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
+5
View File
@@ -37,12 +37,17 @@ func main() {
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",
},
},
}
+28 -17
View File
@@ -3,25 +3,31 @@ module github.com/mayswind/ezbookkeeping
go 1.20
require (
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.0
github.com/go-playground/validator/v10 v10.14.0
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.0
github.com/stretchr/testify v1.8.2
github.com/urfave/cli/v2 v2.25.4
golang.org/x/crypto v0.9.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/boombuler/barcode v1.0.1 // indirect
github.com/bytedance/sonic v1.8.0 // indirect
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
@@ -29,26 +35,31 @@ require (
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.0 // 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.0.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.17 // 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.6 // 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.9 // 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.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.28.1 // 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
)
+89 -24
View File
@@ -1,5 +1,6 @@
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=
@@ -22,6 +23,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
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=
@@ -29,9 +32,13 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
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.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
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=
@@ -75,13 +82,15 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
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.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
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=
@@ -89,6 +98,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
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/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=
@@ -96,8 +106,10 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
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.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
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=
@@ -105,8 +117,8 @@ github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
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.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/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=
@@ -128,16 +140,20 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
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/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=
@@ -167,6 +183,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
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/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -220,6 +237,7 @@ github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
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=
@@ -227,20 +245,24 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
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/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 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
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=
@@ -255,6 +277,8 @@ 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=
@@ -267,13 +291,15 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
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-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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=
@@ -301,7 +327,9 @@ github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtb
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=
@@ -316,8 +344,8 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw
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.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
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=
@@ -347,10 +375,14 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
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=
@@ -369,8 +401,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
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.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@@ -394,8 +426,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
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/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -403,12 +437,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
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.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
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.4 h1:HyYwPrTO3im9rYhUff/ZNs78eolxt0nJ4LN+9yJKSH4=
github.com/urfave/cli/v2 v2.25.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
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=
@@ -431,8 +467,9 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
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 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
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=
@@ -451,6 +488,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
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=
@@ -460,6 +499,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
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=
@@ -518,10 +558,13 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
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.0.0-20220811171246-fbc7d0a398ab/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=
@@ -531,6 +574,8 @@ 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=
@@ -550,11 +595,13 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
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=
@@ -576,25 +623,33 @@ google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/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/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.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=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -604,6 +659,7 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
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=
@@ -618,6 +674,7 @@ 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=
@@ -653,6 +710,7 @@ modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/E
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=
@@ -690,17 +748,24 @@ 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=
+3457 -1773
View File
File diff suppressed because it is too large Load Diff
+21 -12
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"repository": {
"type": "git",
@@ -18,38 +18,47 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"@vuepic/vue-datepicker": "^5.1.2",
"@mdi/js": "^7.2.96",
"@vuepic/vue-datepicker": "^5.4.0",
"axios": "^1.4.0",
"cbor-js": "^0.1.0",
"clipboard": "^2.0.11",
"crypto-js": "^4.1.1",
"dom7": "^4.0.6",
"framework7": "^8.0.5",
"echarts": "^5.4.3",
"framework7": "^8.3.0",
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.0.5",
"framework7-vue": "^8.3.0",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"line-awesome": "^1.3.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"pinia": "^2.1.6",
"register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^9.3.2",
"swiper": "^10.2.0",
"ua-parser-js": "^1.0.35",
"vue": "^3.3.4",
"vue-echarts": "^6.6.1",
"vue-i18n": "^9.2.2",
"vuex": "^4.1.0"
"vue-router": "^4.2.4",
"vue3-perfect-scrollbar": "^1.6.1",
"vuedraggable": "^4.1.0",
"vuetify": "^3.3.16"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.0",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.4",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-plugin-vue": "^9.14.1",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"git-rev-sync": "^3.0.2",
"postcss-preset-env": "^8.4.1",
"vite": "^4.3.5",
"vite-plugin-pwa": "^0.15.1"
"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%",
+23 -16
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,7 +34,7 @@ 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())
@@ -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,7 +94,7 @@ 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())
@@ -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,7 +193,7 @@ 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())
@@ -196,7 +203,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
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,7 +236,7 @@ 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())
@@ -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())
+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)
+16 -16
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())
@@ -119,30 +119,30 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
}
// DataStatisticsHandler returns user data statistics
func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
totalAccountCount, err := a.accounts.GetTotalAccountCountByUid(uid)
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(uid)
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(uid)
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(uid)
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())
@@ -160,7 +160,7 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (interface{}
}
// 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)
@@ -170,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) {
@@ -184,21 +184,21 @@ 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.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.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())
+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
}
+12 -2
View File
@@ -1,6 +1,7 @@
package api
import (
"crypto/tls"
"io"
"net/http"
"sort"
@@ -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()
+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
}
+1 -1
View File
@@ -15,7 +15,7 @@ var (
)
// HealthStatusHandler returns the health status of current service
func (a *HealthsApi) HealthStatusHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *HealthsApi) HealthStatusHandler(c *core.Context) (any, *errs.Error) {
result := make(map[string]string)
result["version"] = settings.Version
+35 -4
View File
@@ -5,12 +5,19 @@ import (
"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 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 {
@@ -21,14 +28,38 @@ var (
MapImages = &MapImageProxy{}
)
// OpenStreetMapTileImageProxyHandler returns open street map tile image
func (p *MapImageProxy) OpenStreetMapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
// 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(openStreetMapTileImageUrlFormat, zoomLevel, coordinateX, fileName)
imageRawUrl := fmt.Sprintf(targetUrl, zoomLevel, coordinateX, fileName)
imageUrl, _ := url.Parse(imageRawUrl)
req.URL = imageUrl
+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
}
+13 -12
View File
@@ -26,9 +26,9 @@ 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())
@@ -61,7 +61,7 @@ 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) {
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (any, *errs.Error) {
_, claims, err := a.tokens.ParseTokenByHeader(c)
if err != nil {
@@ -82,7 +82,7 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *er
}
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, claims.Uid, err.Error())
@@ -94,7 +94,7 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *er
}
// 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)
@@ -120,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())
@@ -132,9 +132,9 @@ 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())
@@ -155,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())
@@ -167,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())
@@ -191,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)
+161 -148
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,7 +35,7 @@ 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())
@@ -44,7 +46,7 @@ func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (interfa
}
// 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,7 +56,7 @@ 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())
@@ -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())
@@ -105,9 +107,9 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
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 {
@@ -117,7 +119,7 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
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,6 +145,149 @@ func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (
uid := c.GetCurrentUid()
categories, err := a.createBatchCategories(c, uid, &categoryCreateBatchReq)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return a.getTransactionCategoryListByTypeResponse(categories, 0)
}
// CategoryModifyHandler saves an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any, *errs.Error) {
var categoryModifyReq models.TransactionCategoryModifyRequest
err := c.ShouldBindJSON(&categoryModifyReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
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.Or(err, errs.ErrOperationFailed)
}
newCategory := &models.TransactionCategory{
CategoryId: category.CategoryId,
Uid: uid,
Name: categoryModifyReq.Name,
Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment,
Hidden: categoryModifyReq.Hidden,
}
if newCategory.Name == category.Name &&
newCategory.Icon == category.Icon &&
newCategory.Color == category.Color &&
newCategory.Comment == category.Comment &&
newCategory.Hidden == category.Hidden {
return nil, errs.ErrNothingWillBeUpdated
}
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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
newCategory.Type = category.Type
newCategory.ParentCategoryId = category.ParentCategoryId
newCategory.DisplayOrder = category.DisplayOrder
categoryResp := newCategory.ToTransactionCategoryInfoResponse()
return categoryResp, nil
}
// CategoryHideHandler hides an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (any, *errs.Error) {
var categoryHideReq models.TransactionCategoryHideRequest
err := c.ShouldBindJSON(&categoryHideReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryHideHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, categoryHideReq.Id)
return true, nil
}
// CategoryMoveHandler moves display order of existed transaction categories by request parameters for current user
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (any, *errs.Error) {
var categoryMoveReq models.TransactionCategoryMoveRequest
err := c.ShouldBindJSON(&categoryMoveReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
categories := make([]*models.TransactionCategory, len(categoryMoveReq.NewDisplayOrders))
for i := 0; i < len(categoryMoveReq.NewDisplayOrders); i++ {
newDisplayOrder := categoryMoveReq.NewDisplayOrders[i]
category := &models.TransactionCategory{
Uid: uid,
CategoryId: newDisplayOrder.Id,
DisplayOrder: newDisplayOrder.DisplayOrder,
}
categories[i] = category
}
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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
return true, nil
}
// CategoryDeleteHandler deletes an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (any, *errs.Error) {
var categoryDeleteReq models.TransactionCategoryDeleteRequest
err := c.ShouldBindJSON(&categoryDeleteReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] user \"uid:%d\" has deleted category \"id:%d\"", uid, categoryDeleteReq.Id)
return true, nil
}
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))
@@ -153,7 +298,7 @@ func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (
var maxOrderId, exists = categoryTypeMaxOrderMap[categoryCreateReq.Type]
if !exists {
maxOrderId, err = a.categories.GetMaxDisplayOrder(uid, categoryCreateReq.Type)
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())
@@ -181,148 +326,16 @@ func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (
totalCount++
}
categories, err := a.categories.CreateCategories(uid, categoriesMap)
categories, err := a.categories.CreateCategories(c, uid, categoriesMap)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
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.CategoryCreateBatchHandler] user \"uid:%d\" has created categoroies successfully", uid)
log.InfofWithRequestId(c, "[transaction_categories.createBatchCategories] user \"uid:%d\" has created categories 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) {
var categoryModifyReq models.TransactionCategoryModifyRequest
err := c.ShouldBindJSON(&categoryModifyReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
category, err := a.categories.GetCategoryByCategoryId(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.Or(err, errs.ErrOperationFailed)
}
newCategory := &models.TransactionCategory{
CategoryId: category.CategoryId,
Uid: uid,
Name: categoryModifyReq.Name,
Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment,
Hidden: categoryModifyReq.Hidden,
}
if newCategory.Name == category.Name &&
newCategory.Icon == category.Icon &&
newCategory.Color == category.Color &&
newCategory.Comment == category.Comment &&
newCategory.Hidden == category.Hidden {
return nil, errs.ErrNothingWillBeUpdated
}
err = a.categories.ModifyCategory(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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
newCategory.Type = category.Type
newCategory.ParentCategoryId = category.ParentCategoryId
newCategory.DisplayOrder = category.DisplayOrder
categoryResp := newCategory.ToTransactionCategoryInfoResponse()
return categoryResp, nil
}
// CategoryHideHandler hides an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interface{}, *errs.Error) {
var categoryHideReq models.TransactionCategoryHideRequest
err := c.ShouldBindJSON(&categoryHideReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryHideHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.categories.HideCategory(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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, categoryHideReq.Id)
return true, nil
}
// CategoryMoveHandler moves display order of existed transaction categories by request parameters for current user
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interface{}, *errs.Error) {
var categoryMoveReq models.TransactionCategoryMoveRequest
err := c.ShouldBindJSON(&categoryMoveReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
categories := make([]*models.TransactionCategory, len(categoryMoveReq.NewDisplayOrders))
for i := 0; i < len(categoryMoveReq.NewDisplayOrders); i++ {
newDisplayOrder := categoryMoveReq.NewDisplayOrders[i]
category := &models.TransactionCategory{
Uid: uid,
CategoryId: newDisplayOrder.Id,
DisplayOrder: newDisplayOrder.DisplayOrder,
}
categories[i] = category
}
err = a.categories.ModifyCategoryDisplayOrders(uid, categories)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
return true, nil
}
// CategoryDeleteHandler deletes an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
var categoryDeleteReq models.TransactionCategoryDeleteRequest
err := c.ShouldBindJSON(&categoryDeleteReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.categories.DeleteCategory(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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] user \"uid:%d\" has deleted category \"id:%d\"", uid, categoryDeleteReq.Id)
return true, nil
return categories, nil
}
func (a *TransactionCategoriesApi) createNewCategoryModel(uid int64, categoryCreateReq *models.TransactionCategoryCreateRequest, order int32) *models.TransactionCategory {
+16 -16
View File
@@ -23,9 +23,9 @@ 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())
@@ -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,7 +54,7 @@ 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())
@@ -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,7 +78,7 @@ 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())
@@ -87,7 +87,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (interface{}, *er
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,7 +112,7 @@ 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())
@@ -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())
+78 -64
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,21 +48,21 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{},
uid := c.GetCurrentUid()
allAccountIds, err := a.getAccountOrSubAccountIds(transactionCountReq.AccountId, 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(transactionCountReq.CategoryId, uid)
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.Or(err, errs.ErrOperationFailed)
}
totalCount, err := a.transactions.GetTransactionCount(uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, 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())
@@ -75,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)
@@ -92,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) {
@@ -102,21 +104,32 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.getAccountOrSubAccountIds(transactionListReq.AccountId, 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(transactionListReq.CategoryId, uid)
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.Or(err, errs.ErrOperationFailed)
}
transactions, err := a.transactions.GetTransactionsByMaxTime(uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, 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())
@@ -147,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)
@@ -168,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) {
@@ -178,34 +195,27 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.getAccountOrSubAccountIds(transactionListReq.AccountId, 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(transactionListReq.CategoryId, uid)
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.Or(err, errs.ErrOperationFailed)
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, 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.Or(err, errs.ErrOperationFailed)
}
totalCount, err := a.transactions.GetMonthTransactionCount(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, 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.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionListResult(c, user, transactions, utcOffset, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
@@ -215,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)
@@ -232,7 +242,7 @@ 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())
@@ -259,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)
@@ -280,14 +290,14 @@ 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 {
@@ -295,12 +305,12 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
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())
@@ -353,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)
@@ -395,7 +407,7 @@ 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 {
@@ -403,7 +415,7 @@ func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (inter
return nil, errs.Or(err, errs.ErrOperationFailed)
}
totalAmounts, err := a.transactions.GetAccountsMonthTotalIncomeAndExpense(uid, startTime, endTime, pageCountForLoadTransactionAmounts)
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())
@@ -463,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,
@@ -482,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)
@@ -499,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) {
@@ -509,7 +523,7 @@ 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())
@@ -528,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)
@@ -542,7 +556,7 @@ 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())
@@ -553,7 +567,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
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())
@@ -562,7 +576,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
}
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())
@@ -598,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)
@@ -638,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) {
@@ -655,7 +669,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
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())
@@ -670,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)
@@ -687,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) {
@@ -697,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())
@@ -709,7 +723,7 @@ 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())
@@ -774,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())
@@ -790,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)
@@ -807,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) {
@@ -817,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())
@@ -835,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())
@@ -870,11 +884,11 @@ func (a *TransactionsApi) filterTransactions(c *core.Context, uid int64, transac
return finalTransactions
}
func (a *TransactionsApi) getAccountOrSubAccountIds(accountId 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(uid, accountId)
allSubAccounts, err := a.accounts.GetSubAccountsByAccountId(c, uid, accountId)
if err != nil {
return nil, err
@@ -892,11 +906,11 @@ func (a *TransactionsApi) getAccountOrSubAccountIds(accountId int64, uid int64)
return allAccountIds, nil
}
func (a *TransactionsApi) getCategoryOrSubCategoryIds(categoryId int64, uid int64) ([]int64, error) {
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
@@ -963,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())
@@ -972,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())
@@ -983,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())
@@ -992,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())
+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())
+242 -16
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"
@@ -30,13 +32,13 @@ var (
)
// 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())
@@ -63,7 +65,7 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
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())
@@ -72,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())
@@ -85,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)
@@ -92,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) {
@@ -110,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)
@@ -120,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) {
@@ -163,7 +262,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
}
if userUpdateReq.DefaultAccountId > 0 && userUpdateReq.DefaultAccountId != user.DefaultAccountId {
accounts, err := a.accounts.GetAccountsByAccountIds(uid, []int64{userUpdateReq.DefaultAccountId})
accounts, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
if err != nil || len(accounts) < 1 {
return nil, errs.Or(err, errs.ErrUserDefaultAccountIsInvalid)
@@ -241,22 +340,48 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
return nil, errs.ErrNothingWillBeUpdated
}
keyProfileUpdated, err := a.users.UpdateUser(userNew, modifyUserLanguage)
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)
@@ -264,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())
@@ -272,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)
@@ -281,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, false)
_, _, 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())
+28 -1
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)
}
@@ -64,6 +84,13 @@ func (c *Context) GetCurrentUid() int64 {
return claims.Uid
}
// GetClientLocale returns the client locale name
func (c *Context) GetClientLocale() string {
value := c.GetHeader(AcceptLanguageHeaderName)
return value
}
// GetClientTimezoneOffset returns the client timezone offset
func (c *Context) GetClientTimezoneOffset() (int16, error) {
value := c.GetHeader(ClientTimezoneOffsetHeaderName)
+1 -1
View File
@@ -10,7 +10,7 @@ import (
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)
+4 -2
View File
@@ -11,8 +11,10 @@ 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
+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
+6 -9
View File
@@ -2,6 +2,7 @@ package datastore
import (
"fmt"
"net"
"net/url"
"os"
"strings"
@@ -103,14 +104,14 @@ func initializeDatabase(dbConfig *settings.DatabaseConfig) (*Database, error) {
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...)
}
+15
View File
@@ -14,6 +14,7 @@ const (
SystemSubcategoryDefault = 0
SystemSubcategorySetting = 1
SystemSubcategoryDatabase = 2
SystemSubcategoryMail = 3
)
// Sub categories of normal error
@@ -37,6 +38,7 @@ type Error struct {
HttpStatusCode int
Message string
BaseError []error
Context any
}
// Error returns the error 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 -6
View File
@@ -4,10 +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")
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid map provider")
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 -13
View File
@@ -6,17 +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")
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
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")
)
+7
View File
@@ -22,4 +22,11 @@ var (
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 {
+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)
}
}
+73 -43
View File
@@ -17,58 +17,24 @@ type TokenSourceType byte
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, TOKEN_SOURCE_TYPE_HEADER)
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()
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) {
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
jwtAuthorization(c, TOKEN_SOURCE_TYPE_ARGUMENT)
}
if err != nil {
utils.PrintJsonErrorResult(c, err)
return
}
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorizationByQueryString] 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.JWTAuthorizationByQueryString] user \"uid:%d\" token type is invalid", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
c.SetTokenClaims(claims)
c.Next()
// 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
@@ -90,6 +56,68 @@ func JWTTwoFactorAuthorization(c *core.Context) {
c.Next()
}
// 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)
@@ -114,6 +142,8 @@ func getTokenClaims(c *core.Context, source TokenSourceType) (*core.UserTokenCla
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)
+50 -3
View File
@@ -1,7 +1,9 @@
package middlewares
import (
"encoding/base64"
"fmt"
"net/url"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -15,9 +17,48 @@ 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", config.MapProvider),
buildBooleanSetting("mp", config.EnableMapDataFetchProxy),
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, "_")
@@ -28,7 +69,13 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
}
func buildStringSetting(key string, value string) string {
return fmt.Sprintf("%s.%s", key, strings.Replace(value, ".", "-", -1))
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 {
+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"`
}
+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"`
}
+21 -2
View File
@@ -112,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"`
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"`
@@ -126,8 +128,6 @@ type TransactionListInMonthByPageRequest struct {
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
Page int32 `form:"page" binding:"required,min=1"`
Count int32 `form:"count" binding:"required,min=1,max=50"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
@@ -218,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
@@ -451,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
}
+46
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -63,6 +64,7 @@ type User struct {
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
@@ -76,6 +78,8 @@ 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"`
@@ -85,6 +89,7 @@ type UserBasicInfo struct {
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
EmailVerified bool `json:"emailVerified"`
}
// UserLoginRequest represents all parameters of user login request
@@ -102,6 +107,24 @@ type UserRegisterRequest struct {
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
@@ -132,6 +155,8 @@ 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"`
@@ -141,6 +166,7 @@ type UserProfileResponse struct {
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
EmailVerified bool `json:"emailVerified"`
LastLoginAt int64 `json:"lastLoginAt"`
}
@@ -192,6 +218,8 @@ 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,
@@ -201,6 +229,7 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo {
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
EmailVerified: u.EmailVerified,
}
}
@@ -210,6 +239,8 @@ 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,
@@ -219,6 +250,21 @@ func (u *User) ToUserProfileResponse() *UserProfileResponse {
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)
+43 -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"
@@ -31,30 +32,30 @@ var (
)
// GetTotalAccountCountByUid returns total account count of user
func (s *AccountService) GetTotalAccountCountByUid(uid int64) (int64, error) {
func (s *AccountService) GetTotalAccountCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).Count(&models.Account{})
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
}
@@ -64,13 +65,13 @@ 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(uid int64, accountId int64) ([]*models.Account, error) {
func (s *AccountService) GetSubAccountsByAccountId(c *core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -80,13 +81,13 @@ func (s *AccountService) GetSubAccountsByAccountId(uid int64, accountId int64) (
}
var accounts []*models.Account
err := s.UserDataDB(uid).Where("uid=? AND deleted=? AND parent_account_id=?", uid, false, accountId).OrderBy("display_order asc").Find(&accounts)
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
}
@@ -96,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
@@ -107,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) (int32, 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
@@ -127,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) (int32, error) {
func (s *AccountService) GetMaxSubAccountDisplayOrder(c *core.Context, uid int64, category models.AccountCategory, parentAccountId int64) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
@@ -137,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
@@ -151,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
}
@@ -162,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
@@ -184,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,
@@ -203,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)
@@ -227,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
}
@@ -238,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)
@@ -255,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
}
@@ -267,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 {
@@ -281,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
}
@@ -290,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)
@@ -307,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
}
@@ -320,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)
+16
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
+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
}
+63 -25
View File
@@ -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,7 +58,7 @@ 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
}
@@ -73,18 +73,33 @@ func (s *TokenService) ParseTokenByArgument(c *core.Context, tokenParameterName
return s.parseToken(c, request.ArgumentExtractor{tokenParameterName})
}
// 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
}
@@ -93,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 {
@@ -107,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{})
@@ -129,32 +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 {
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{
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, ":")
@@ -199,7 +237,7 @@ func (s *TokenService) parseToken(c *core.Context, extractor request.Extractor)
claims := &core.UserTokenClaims{}
token, err := request.ParseFromRequest(c.Request, extractor,
func(token *jwt.Token) (interface{}, error) {
func(token *jwt.Token) (any, error) {
now := time.Now().Unix()
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
@@ -208,7 +246,7 @@ func (s *TokenService) parseToken(c *core.Context, extractor request.Extractor)
return nil, errs.ErrInvalidUserTokenId
}
tokenRecord, err := s.getTokenRecord(claims.Uid, userTokenId, claims.IssuedAt)
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())
@@ -251,7 +289,7 @@ func (s *TokenService) parseToken(c *core.Context, extractor request.Extractor)
return token, claims, err
}
func (s *TokenService) createToken(user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, error) {
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()
@@ -284,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
@@ -293,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
}
@@ -303,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
@@ -316,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
}
@@ -325,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
})
+41 -27
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,24 +31,24 @@ var (
)
// GetTotalCategoryCountByUid returns total category count of user
func (s *TransactionCategoryService) GetTotalCategoryCountByUid(uid int64) (int64, error) {
func (s *TransactionCategoryService) GetTotalCategoryCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).Count(&models.TransactionCategory{})
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)
@@ -62,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
}
@@ -78,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
@@ -90,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
}
@@ -100,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
@@ -111,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) (int32, 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
@@ -131,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) (int32, error) {
func (s *TransactionCategoryService) GetMaxSubCategoryDisplayOrder(c *core.Context, uid int64, categoryType models.TransactionCategoryType, parentCategoryId int64) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
@@ -141,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
@@ -155,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
}
@@ -185,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()
@@ -196,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
@@ -206,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)
@@ -227,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 {
@@ -248,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
}
@@ -260,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 {
@@ -274,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
}
@@ -283,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)
@@ -300,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
}
@@ -312,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)
@@ -349,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
}
@@ -361,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 {
+35 -30
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,30 +31,30 @@ var (
)
// GetTotalTagCountByUid returns total tag count of user
func (s *TransactionTagService) GetTotalTagCountByUid(uid int64) (int64, error) {
func (s *TransactionTagService) GetTotalTagCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).Count(&models.TransactionTag{})
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
}
@@ -63,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
@@ -75,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
}
@@ -85,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
@@ -96,13 +97,13 @@ func (s *TransactionTagService) GetTagsByTagIds(uid int64, tagIds []int64) (map[
}
// GetMaxDisplayOrder returns the max display order
func (s *TransactionTagService) GetMaxDisplayOrder(uid int64) (int32, 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
@@ -116,13 +117,13 @@ func (s *TransactionTagService) GetMaxDisplayOrder(uid int64) (int32, 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)
@@ -130,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)
@@ -144,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
@@ -159,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
@@ -185,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 {
@@ -199,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
}
@@ -211,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 {
@@ -225,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
}
@@ -234,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)
@@ -251,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
}
@@ -263,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 {
@@ -285,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
}
@@ -297,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 {
@@ -317,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
+86 -51
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"
@@ -33,23 +34,23 @@ var (
)
// GetTotalTransactionCountByUid returns total transaction count of user
func (s *TransactionService) GetTotalTransactionCountByUid(uid int64) (int64, error) {
func (s *TransactionService) GetTotalTransactionCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).Count(&models.Transaction{})
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 int32, 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
@@ -69,16 +70,22 @@ func (s *TransactionService) GetAllTransactions(uid int64, pageCount int32, noDu
}
// GetAllTransactionsByMaxTime returns all transactions before given time
func (s *TransactionService) GetAllTransactionsByMaxTime(uid int64, maxTransactionTime int64, count int32, noDuplicated bool) ([]*models.Transaction, error) {
return s.GetTransactionsByMaxTime(uid, maxTransactionTime, 0, 0, nil, nil, "", 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, accountIds []int64, keyword string, count int32, 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
}
@@ -86,47 +93,58 @@ func (s *TransactionService) GetTransactionsByMaxTime(uid int64, maxTransactionT
var transactions []*models.Transaction
var err error
actualCount := count
if needOneMoreItem {
actualCount++
}
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, noDuplicated)
err = s.UserDataDB(uid).Where(condition, conditionParams...).Limit(int(count), 0).OrderBy("transaction_time desc").Find(&transactions)
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 int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, page int32, count int32, 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, accountIds, keyword, true)
err = s.UserDataDB(uid).Where(condition, conditionParams...).Limit(int(count), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
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
}
@@ -136,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
@@ -148,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, nil, "")
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 int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []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
}
@@ -169,21 +187,21 @@ func (s *TransactionService) GetMonthTransactionCount(uid int64, year int32, mon
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startTime.Unix())
maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endTime.Unix()) - 1
return s.GetTransactionCount(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, 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, accountIds []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, accountIds, keyword, true)
return s.UserDataDB(uid).Where(condition, conditionParams...).Count(&models.Transaction{})
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
}
@@ -204,6 +222,11 @@ func (s *TransactionService) CreateTransaction(transaction *models.Transaction,
}
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 {
@@ -219,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],
@@ -230,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)
@@ -394,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
}
@@ -413,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],
@@ -424,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)
@@ -765,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
}
@@ -782,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)
@@ -885,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
}
@@ -908,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)
@@ -975,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
}
@@ -984,7 +1019,7 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(uid int64, startUn
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
var transactionTotalAmounts []*models.Transaction
err := s.UserDataDB(uid).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)
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
@@ -1007,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
}
@@ -1022,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
@@ -1074,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)
@@ -1097,7 +1132,7 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(uid i
}
var transactionTotalAmounts []*models.Transaction
err := s.UserDataDB(uid).Select("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
@@ -1118,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, accountIds []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)
+17 -16
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
})
@@ -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
})
+201 -29
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, modifyUserLanguage bool) (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 != "" {
@@ -225,7 +251,7 @@ func (s *UserService) UpdateUser(user *models.User, modifyUserLanguage bool) (ke
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 {
@@ -238,26 +264,118 @@ func (s *UserService) UpdateUser(user *models.User, modifyUserLanguage bool) (ke
})
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
}
@@ -269,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
@@ -280,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
+152 -22
View File
@@ -62,9 +62,29 @@ const (
InternalUuidGeneratorType string = "internal"
)
// User avatar provider types
const (
GravatarProvider string = "gravatar"
)
// Map provider types
const (
OpenStreetMapProvider string = "openstreetmap"
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
@@ -93,9 +113,11 @@ const (
defaultLogMode string = "console"
defaultLoglevel Level = LOGLEVEL_INFO
defaultSecretKey string = "ezbookkeeping"
defaultTokenExpiredTime uint32 = 604800 // 7 days
defaultTemporaryTokenExpiredTime uint32 = 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 uint32 = 10000 // 10 seconds
)
@@ -117,6 +139,15 @@ type DatabaseConfig struct {
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
type Config struct {
// Global
@@ -147,6 +178,10 @@ type Config struct {
EnableQueryLog bool
AutoUpdateDatabase bool
// Mail
EnableSMTP bool
SMTPConfig *SMTPConfig
// Log
LogModes []string
EnableConsoleLog bool
@@ -160,27 +195,44 @@ type Config struct {
UuidServerId uint8
// Secret
SecretKey string
EnableTwoFactor bool
TokenExpiredTime uint32
TokenExpiredTimeDuration time.Duration
TemporaryTokenExpiredTime uint32
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
EnableMapDataFetchProxy bool
MapProvider string
TomTomMapAPIKey string
GoogleMapAPIKey string
BaiduMapAK string
AmapApplicationKey string
AmapSecurityVerificationMethod string
AmapApplicationSecret string
AmapApiExternalProxyUrl string
EnableMapDataFetchProxy bool
// Exchange Rates
ExchangeRatesDataSource string
ExchangeRatesRequestTimeout uint32
ExchangeRatesSkipTLSVerify bool
}
// LoadConfiguration loads setting config from given config file path
@@ -218,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 {
@@ -366,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), " ")
@@ -414,6 +488,12 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
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
@@ -421,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
}
@@ -432,36 +522,76 @@ func loadDataConfiguration(config *Config, configFile *ini.File, sectionName str
}
func loadMapConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "map_provider") == "" {
mapProvider := getConfigItemStringValue(configFile, sectionName, "map_provider")
if mapProvider == "" {
config.MapProvider = ""
} else if getConfigItemStringValue(configFile, sectionName, "map_provider") == OpenStreetMapProvider {
} 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 getConfigItemStringValue(configFile, sectionName, "data_source") == MonetaryAuthorityOfSingaporeDataSource {
} else if dataSource == MonetaryAuthorityOfSingaporeDataSource {
config.ExchangeRatesDataSource = MonetaryAuthorityOfSingaporeDataSource
} else {
return errs.ErrInvalidExchangeRatesDataSource
}
config.ExchangeRatesRequestTimeout = getConfigItemUint32Value(configFile, sectionName, "request_timeout", defaultExchangeRatesDataRequestTimeout)
config.ExchangeRatesSkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "skip_tls_verify", false)
return nil
}
+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)
}
+14
View File
@@ -0,0 +1,14 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetGravatarUrl(t *testing.T) {
// Reference: https://en.gravatar.com/site/implement/hash/
expectedValue := "https://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346"
actualValue := GetGravatarUrl("MyEmailAddress@example.com")
assert.Equal(t, expectedValue, actualValue)
}
+38
View File
@@ -13,6 +13,8 @@ const (
longDateTimeWithoutSecondFormat = "2006-01-02 15:04"
shortDateTimeFormat = "2006-1-2 15:4:5"
yearMonthDateTimeFormat = "2006-01"
westernmostTimezoneUtcOffset = -720 // Etc/GMT+12 (UTC-12:00)
easternmostTimezoneUtcOffset = 840 // Pacific/Kiritimati (UTC+14:00)
)
// FormatUnixTimeToLongDateTimeInServerTimezone returns a textual representation of the unix time formatted by long date time format
@@ -42,6 +44,18 @@ func FormatUnixTimeToYearMonth(unixTime int64, timezone *time.Location) string {
return t.Format(yearMonthDateTimeFormat)
}
// ParseFromLongDateTimeToMinUnixTime parses a formatted string in long date time format to minimal unix time (the westernmost timezone)
func ParseFromLongDateTimeToMinUnixTime(t string) (time.Time, error) {
timezone := time.FixedZone("Timezone", easternmostTimezoneUtcOffset*60)
return time.ParseInLocation(longDateTimeFormat, t, timezone)
}
// ParseFromLongDateTimeToMaxUnixTime parses a formatted string in long date time format to maximal unix time (the easternmost timezone)
func ParseFromLongDateTimeToMaxUnixTime(t string) (time.Time, error) {
timezone := time.FixedZone("Timezone", westernmostTimezoneUtcOffset*60)
return time.ParseInLocation(longDateTimeFormat, t, timezone)
}
// ParseFromLongDateTime parses a formatted string in long date time format
func ParseFromLongDateTime(t string, utcOffset int16) (time.Time, error) {
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
@@ -59,6 +73,30 @@ func ParseFromShortDateTime(t string, utcOffset int16) (time.Time, error) {
return time.ParseInLocation(shortDateTimeFormat, t, timezone)
}
func ParseFromElapsedSeconds(elapsedSeconds int) (string, error) {
if elapsedSeconds < 0 || elapsedSeconds >= 86400 {
return "", errs.ErrFormatInvalid
}
second := elapsedSeconds % 60
elapsedSeconds = elapsedSeconds - second
elapsedSeconds = elapsedSeconds / 60
minute := elapsedSeconds % 60
elapsedSeconds = elapsedSeconds - minute
elapsedSeconds = elapsedSeconds / 60
hour := elapsedSeconds
return fmt.Sprintf("%02d:%02d:%02d", hour, minute, second), nil
}
// IsUnixTimeEqualsYearAndMonth returns whether year and month of the unix time are equals to the specified year and month
func IsUnixTimeEqualsYearAndMonth(unixTime int64, timezone *time.Location, year int32, month int32) bool {
date := parseFromUnixTime(unixTime).In(timezone)
return date.Year() == int(year) && int(date.Month()) == int(month)
}
// GetTimezoneOffsetMinutes returns offset minutes according specified timezone
func GetTimezoneOffsetMinutes(timezone *time.Location) int16 {
_, tzOffset := time.Now().In(timezone).Zone()
+70
View File
@@ -35,6 +35,24 @@ func TestFormatUnixTimeToYearMonth(t *testing.T) {
assert.Equal(t, expectedValue, actualValue)
}
func TestParseFromLongDateTimeToMinUnixTime(t *testing.T) {
expectedValue := int64(1690797600)
actualTime, err := ParseFromLongDateTimeToMinUnixTime("2023-08-01 00:00:00")
assert.Equal(t, nil, err)
actualValue := actualTime.Unix()
assert.Equal(t, expectedValue, actualValue)
}
func TestParseFromLongDateTimeToMaxUnixTime(t *testing.T) {
expectedValue := int64(1690891200)
actualTime, err := ParseFromLongDateTimeToMaxUnixTime("2023-08-01 00:00:00")
assert.Equal(t, nil, err)
actualValue := actualTime.Unix()
assert.Equal(t, expectedValue, actualValue)
}
func TestParseFromLongDateTime(t *testing.T) {
expectedValue := int64(1617228083)
actualTime, err := ParseFromLongDateTime("2021-04-01 06:01:23", 480)
@@ -44,6 +62,15 @@ func TestParseFromLongDateTime(t *testing.T) {
assert.Equal(t, expectedValue, actualValue)
}
func TestParseFromLongDateTimeWithoutSecond(t *testing.T) {
expectedValue := int64(1691947440)
actualTime, err := ParseFromLongDateTimeWithoutSecond("2023-08-13 17:24", time.UTC)
assert.Equal(t, nil, err)
actualValue := actualTime.Unix()
assert.Equal(t, expectedValue, actualValue)
}
func TestParseFromShortDateTime(t *testing.T) {
expectedValue := int64(1617228083)
actualTime, err := ParseFromShortDateTime("2021-4-1 6:1:23", 480)
@@ -53,6 +80,49 @@ func TestParseFromShortDateTime(t *testing.T) {
assert.Equal(t, expectedValue, actualValue)
}
func TestParseFromElapsedSeconds(t *testing.T) {
expectedValue := "00:00:00"
actualValue, err := ParseFromElapsedSeconds(0)
assert.Equal(t, nil, err)
assert.Equal(t, expectedValue, actualValue)
expectedValue = "00:00:09"
actualValue, err = ParseFromElapsedSeconds(9)
assert.Equal(t, nil, err)
assert.Equal(t, expectedValue, actualValue)
expectedValue = "00:01:08"
actualValue, err = ParseFromElapsedSeconds(68)
assert.Equal(t, nil, err)
assert.Equal(t, expectedValue, actualValue)
expectedValue = "01:00:07"
actualValue, err = ParseFromElapsedSeconds(3607)
assert.Equal(t, nil, err)
assert.Equal(t, expectedValue, actualValue)
expectedValue = "23:59:59"
actualValue, err = ParseFromElapsedSeconds(86399)
assert.Equal(t, nil, err)
assert.Equal(t, expectedValue, actualValue)
}
func TestParseFromElapsedSeconds_InvalidTime(t *testing.T) {
_, err := ParseFromElapsedSeconds(-1)
assert.NotEqual(t, nil, err)
_, err = ParseFromElapsedSeconds(86400)
assert.NotEqual(t, nil, err)
}
func TestIsUnixTimeEqualsYearAndMonth(t *testing.T) {
actualValue := IsUnixTimeEqualsYearAndMonth(1691947440, time.UTC, 2023, 8)
assert.Equal(t, true, actualValue)
actualValue = IsUnixTimeEqualsYearAndMonth(1690847999, time.UTC, 2023, 8)
assert.Equal(t, false, actualValue)
}
func TestGetTimezoneOffsetMinutes(t *testing.T) {
timezone := time.FixedZone("Test Timezone", 120*60)
expectedValue := int16(120)
+20
View File
@@ -0,0 +1,20 @@
package utils
import (
"net/http"
"github.com/golang-jwt/jwt/v5/request"
)
// CookieExtractor extracts a token from request cookies
type CookieExtractor []string
func (e CookieExtractor) ExtractToken(req *http.Request) (string, error) {
for _, arg := range e {
if cookie, _ := req.Cookie(arg); cookie != nil {
return cookie.Value, nil
}
}
return "", request.ErrNoTokenInRequest
}
+2 -2
View File
@@ -8,7 +8,7 @@ import (
)
// Clone deep-clones src object to dst object
func Clone(src, dst interface{}) error {
func Clone(src, dst any) error {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(src); err != nil {
@@ -25,7 +25,7 @@ func Clone(src, dst interface{}) error {
}
// PrintObjectFields prints all fields in specified object
func PrintObjectFields(obj interface{}) {
func PrintObjectFields(obj any) {
if obj == nil {
return
}
+7
View File
@@ -7,6 +7,7 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"strings"
"unicode"
@@ -133,6 +134,12 @@ func MD5Encode(data []byte) []byte {
return m.Sum(nil)
}
// MD5EncodeToString returns a hashed string by md5
func MD5EncodeToString(data []byte) string {
hash := MD5Encode(data)
return hex.EncodeToString(hash)
}
// AESGCMEncrypt returns a encrypted string by aes-gcm
func AESGCMEncrypt(key []byte, plainText []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
+30
View File
@@ -72,6 +72,16 @@ func TestSubString_OverBoundary(t *testing.T) {
assert.Equal(t, expectedValue, actualValue)
}
func TestGetFirstLowerCharString(t *testing.T) {
expectedValue := "fooBar"
actualValue := GetFirstLowerCharString("fooBar")
assert.Equal(t, expectedValue, actualValue)
expectedValue = "fooBar"
actualValue = GetFirstLowerCharString("FooBar")
assert.Equal(t, expectedValue, actualValue)
}
func TestGetRandomString(t *testing.T) {
actualValue, err := GetRandomString(10)
assert.Equal(t, nil, err)
@@ -103,6 +113,26 @@ func TestMD5Encode(t *testing.T) {
assert.Equal(t, expectedValue, actualValue)
}
func TestMD5EncodeToString(t *testing.T) {
str := "foobar"
expectedValue := "3858f62230ac3c915f300c664312c63f"
actualValue := MD5EncodeToString([]byte(str))
assert.Equal(t, expectedValue, actualValue)
str = ""
expectedValue = "d41d8cd98f00b204e9800998ecf8427e"
actualValue = MD5EncodeToString([]byte(str))
assert.Equal(t, expectedValue, actualValue)
}
func TestEncodePassword(t *testing.T) {
password := "foobar"
salt := "salt"
expectedValue := "QrpKShMygoe4Ym4ibnA7cNDzCcSonBkgFl69IrtnDmp3oROft3/Td/DNXjsweosa"
actualValue := EncodePassword(password, salt)
assert.Equal(t, expectedValue, actualValue)
}
func TestEncryptSecretAndDecryptSecret(t *testing.T) {
str := "foo"
key := "bar"
+19 -6
View File
@@ -35,7 +35,7 @@ type InternalUuidInfo struct {
// InternalUuidGenerator represents internal bundled uuid generator
type InternalUuidGenerator struct {
uuidSeqNumbers [1 << internalUuidTypeBits]uint64
uuidSeqNumbers [1 << internalUuidTypeBits]atomic.Uint64
uuidServerId uint8
}
@@ -51,6 +51,11 @@ func NewInternalUuidGenerator(config *settings.Config) (*InternalUuidGenerator,
// GenerateUuid generates a new uuid
func (u *InternalUuidGenerator) GenerateUuid(idType UuidType) int64 {
uuids := u.GenerateUuids(idType, 1)
if len(uuids) < 1 {
return 0
}
return uuids[0]
}
@@ -71,32 +76,40 @@ func (u *InternalUuidGenerator) GenerateUuids(idType UuidType, count uint8) []in
for {
unixTime = uint64(time.Now().Unix())
newLastSeqId = atomic.AddUint64(&u.uuidSeqNumbers[uuidType], uint64(count))
newLastSeqId = u.uuidSeqNumbers[uuidType].Add(uint64(count))
newSeqUnixTime := newLastSeqId >> seqNumberIdBits
if newLastSeqId>>seqNumberIdBits == unixTime {
if unixTime == newSeqUnixTime {
newFirstSeqId = newLastSeqId - uint64(count-1)
break
} else if unixTime < newSeqUnixTime {
continue
}
currentSeqId := newLastSeqId
newFirstSeqId = unixTime << seqNumberIdBits
newLastSeqId = newFirstSeqId + uint64(count-1)
if atomic.CompareAndSwapUint64(&u.uuidSeqNumbers[uuidType], currentSeqId, newLastSeqId) {
if u.uuidSeqNumbers[uuidType].CompareAndSwap(currentSeqId, newLastSeqId) {
break
}
}
for i := 0; i < int(count); i++ {
seqId := (newFirstSeqId + uint64(i)) & seqNumberIdMask
// the internal uuid generator can only generate 524,287 ids per second for specified type
if seqId > internalUuidSeqIdMask {
return nil
}
uuids[i] = u.assembleUuid(unixTime, uuidType, seqId)
}
return uuids
}
// ParseUuidInfo returns a info struct which contains all information in uuid
func (u *InternalUuidGenerator) ParseUuidInfo(uuid int64) *InternalUuidInfo {
func (u *InternalUuidGenerator) parseInternalUuidInfo(uuid int64) *InternalUuidInfo {
seqId := uint32(uuid & internalUuidSeqIdMask)
uuid = uuid >> internalUuidSeqIdBits
+204 -44
View File
@@ -12,16 +12,21 @@ import (
)
func TestGenerateUuid(t *testing.T) {
expectedUnixTime := time.Now().Unix()
expectedUuidServerId := uint8(90)
expectedUuidType := UUID_TYPE_TRANSACTION
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: expectedUuidServerId})
uuid := generator.GenerateUuid(expectedUuidType)
uuidInfo := generator.ParseUuidInfo(uuid)
actualUnixTime := uuidInfo.UnixTime
assert.Equal(t, uint32(expectedUnixTime), actualUnixTime)
generationStartUnixTime := time.Now().Unix()
uuid := generator.GenerateUuid(expectedUuidType)
generationEndUnixTime := time.Now().Unix()
uuidInfo := generator.parseInternalUuidInfo(uuid)
if generationStartUnixTime == generationEndUnixTime {
actualUnixTime := uuidInfo.UnixTime
assert.Equal(t, uint32(generationStartUnixTime), actualUnixTime)
}
actualUuidServerId := uuidInfo.UuidServerId
assert.Equal(t, expectedUuidServerId, actualUuidServerId)
@@ -37,43 +42,53 @@ func TestGenerateUuid(t *testing.T) {
func TestGenerateUuid_MultiType(t *testing.T) {
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 1})
uuid := generator.GenerateUuid(UUID_TYPE_USER)
uuidInfo := generator.ParseUuidInfo(uuid)
uuidInfo := generator.parseInternalUuidInfo(uuid)
expectedSeqId := 0
actualSeqId := uuidInfo.SequentialId
assert.Equal(t, uint32(expectedSeqId), actualSeqId)
uuid = generator.GenerateUuid(UUID_TYPE_ACCOUNT)
uuidInfo = generator.ParseUuidInfo(uuid)
uuidInfo = generator.parseInternalUuidInfo(uuid)
actualSeqId = uuidInfo.SequentialId
assert.Equal(t, uint32(expectedSeqId), actualSeqId)
uuid = generator.GenerateUuid(UUID_TYPE_TRANSACTION)
uuidInfo = generator.ParseUuidInfo(uuid)
uuidInfo = generator.parseInternalUuidInfo(uuid)
actualSeqId = uuidInfo.SequentialId
assert.Equal(t, uint32(expectedSeqId), actualSeqId)
}
func TestGenerateUuid_2000TimesIn2Seconds(t *testing.T) {
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 2})
expectedUnixTime := time.Now().Unix()
for i := 0; i < 1000; i++ {
generationStartUnixTime := time.Now().Unix()
uuid := generator.GenerateUuid(UUID_TYPE_USER)
uuidInfo := generator.ParseUuidInfo(uuid)
generationEndUnixTime := time.Now().Unix()
uuidInfo := generator.parseInternalUuidInfo(uuid)
if generationStartUnixTime == generationEndUnixTime {
assert.Equal(t, uint32(generationStartUnixTime), uuidInfo.UnixTime)
}
assert.Equal(t, uint32(expectedUnixTime), uuidInfo.UnixTime)
assert.Equal(t, uint32(i), uuidInfo.SequentialId)
}
time.Sleep(1 * time.Second)
expectedUnixTime = time.Now().Unix()
for i := 0; i < 1000; i++ {
generationStartUnixTime := time.Now().Unix()
uuid := generator.GenerateUuid(UUID_TYPE_USER)
uuidInfo := generator.ParseUuidInfo(uuid)
generationEndUnixTime := time.Now().Unix()
uuidInfo := generator.parseInternalUuidInfo(uuid)
if generationStartUnixTime == generationEndUnixTime {
assert.Equal(t, uint32(generationStartUnixTime), uuidInfo.UnixTime)
}
assert.Equal(t, uint32(expectedUnixTime), uuidInfo.UnixTime)
assert.Equal(t, uint32(i), uuidInfo.SequentialId)
}
}
@@ -86,21 +101,77 @@ func TestGenerateUuid_10000TimesConcurrent(t *testing.T) {
var waitGroup sync.WaitGroup
for routineIndex := 0; routineIndex < concurrentCount; routineIndex++ {
waitGroup.Add(1)
go func(currentRoutineIndex int) {
waitGroup.Add(1)
for cycle := 0; cycle < 1000; cycle++ {
expectedUnixTime := time.Now().Unix()
generationStartUnixTime := time.Now().Unix()
uuid := generator.GenerateUuid(UUID_TYPE_USER)
uuidInfo := generator.ParseUuidInfo(uuid)
generationEndUnixTime := time.Now().Unix()
if uint32(expectedUnixTime) != uuidInfo.UnixTime {
mutex.Lock()
assert.Equal(t, uint32(expectedUnixTime), uuidInfo.UnixTime)
mutex.Unlock()
uuidInfo := generator.parseInternalUuidInfo(uuid)
if generationStartUnixTime == generationEndUnixTime {
if uint32(generationStartUnixTime) != uuidInfo.UnixTime {
mutex.Lock()
assert.Equal(t, uint32(generationStartUnixTime), uuidInfo.UnixTime)
mutex.Unlock()
}
}
if uuidInfo.SequentialId == 0 {
fmt.Printf("routine#%d generate uuid %d\n", currentRoutineIndex, uuid)
if existedRoutineIndex, exists := generatedIds.Load(uuid); exists {
mutex.Lock()
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, unix time is %d, seq id is %d, existed routine index is %d, current routine index is %d", uuid, uuidInfo.UnixTime, uuidInfo.SequentialId, existedRoutineIndex, currentRoutineIndex))
mutex.Unlock()
}
generatedIds.Store(uuid, currentRoutineIndex)
}
}
waitGroup.Done()
}(routineIndex)
}
waitGroup.Wait()
}
func TestGenerateUuid_1000000TimesConcurrent(t *testing.T) {
concurrentCount := 10
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 3})
var mutex sync.Mutex
var generatedIds sync.Map
var waitGroup sync.WaitGroup
for routineIndex := 0; routineIndex < concurrentCount; routineIndex++ {
waitGroup.Add(1)
go func(currentRoutineIndex int) {
for cycle := 0; cycle < 100000; cycle++ {
if cycle%50000 == 0 { // each server can only generate 500,000 (10 * 50000) uuids in one second
time.Sleep(1000 * time.Millisecond)
}
generationStartUnixTime := time.Now().Unix()
uuid := generator.GenerateUuid(UUID_TYPE_USER)
generationEndUnixTime := time.Now().Unix()
uuidInfo := generator.parseInternalUuidInfo(uuid)
if generationStartUnixTime == generationEndUnixTime {
if uint32(generationStartUnixTime) != uuidInfo.UnixTime {
mutex.Lock()
assert.Equal(t, uint32(generationStartUnixTime), uuidInfo.UnixTime)
mutex.Unlock()
}
}
if uuidInfo.SequentialId == 0 {
fmt.Printf("routine#%d generate uuid %d\n", currentRoutineIndex, uuid)
if existedRoutineIndex, exists := generatedIds.Load(uuid); exists {
mutex.Lock()
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, unix time is %d, seq id is %d, existed routine index is %d, current routine index is %d", uuid, uuidInfo.UnixTime, uuidInfo.SequentialId, existedRoutineIndex, currentRoutineIndex))
@@ -130,19 +201,23 @@ func TestGenerateUuids_Count0(t *testing.T) {
}
func TestGenerateUuids_Count255(t *testing.T) {
expectedUnixTime := time.Now().Unix()
expectedUuidServerId := uint8(90)
expectedUuidType := UUID_TYPE_TRANSACTION
expectedUuidCount := uint8(255)
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: expectedUuidServerId})
generationStartUnixTime := time.Now().Unix()
uuids := generator.GenerateUuids(expectedUuidType, expectedUuidCount)
generationEndUnixTime := time.Now().Unix()
for i := 0; i < int(expectedUuidCount); i++ {
uuidInfo := generator.ParseUuidInfo(uuids[i])
uuidInfo := generator.parseInternalUuidInfo(uuids[i])
actualUnixTime := uuidInfo.UnixTime
assert.Equal(t, uint32(expectedUnixTime), actualUnixTime)
if generationStartUnixTime == generationEndUnixTime {
actualUnixTime := uuidInfo.UnixTime
assert.Equal(t, uint32(generationStartUnixTime), actualUnixTime)
}
actualUuidServerId := uuidInfo.UuidServerId
assert.Equal(t, expectedUuidServerId, actualUuidServerId)
@@ -166,15 +241,18 @@ func TestGenerateUuids_30TimesIn3Seconds(t *testing.T) {
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: expectedUuidServerId})
for cycle := 0; cycle < 30; cycle++ {
expectedUnixTime := time.Now().Unix()
generationStartUnixTime := time.Now().Unix()
uuids := generator.GenerateUuids(expectedUuidType, expectedUuidCount)
generationEndUnixTime := time.Now().Unix()
var firstSeqId uint32
for i := 0; i < int(expectedUuidCount); i++ {
uuidInfo := generator.ParseUuidInfo(uuids[i])
uuidInfo := generator.parseInternalUuidInfo(uuids[i])
actualUnixTime := uuidInfo.UnixTime
assert.Equal(t, uint32(expectedUnixTime), actualUnixTime)
if generationStartUnixTime == generationEndUnixTime {
actualUnixTime := uuidInfo.UnixTime
assert.Equal(t, uint32(generationStartUnixTime), actualUnixTime)
}
actualUuidServerId := uuidInfo.UuidServerId
assert.Equal(t, expectedUuidServerId, actualUuidServerId)
@@ -204,29 +282,36 @@ func TestGenerateUuids_20000TimesConcurrent(t *testing.T) {
var waitGroup sync.WaitGroup
for routineIndex := 0; routineIndex < concurrentCount; routineIndex++ {
go func(currentRoutineIndex int) {
waitGroup.Add(1)
waitGroup.Add(1)
go func(currentRoutineIndex int) {
for cycle := 0; cycle < 100; cycle++ {
expectedUnixTime := time.Now().Unix()
generationStartUnixTime := time.Now().Unix()
uuids := generator.GenerateUuids(UUID_TYPE_USER, expectedUuidCount)
generationEndUnixTime := time.Now().Unix()
for i := 0; i < int(expectedUuidCount); i++ {
uuidInfo := generator.ParseUuidInfo(uuids[i])
uuidInfo := generator.parseInternalUuidInfo(uuids[i])
if uint32(expectedUnixTime) != uuidInfo.UnixTime {
mutex.Lock()
assert.Equal(t, uint32(expectedUnixTime), uuidInfo.UnixTime)
mutex.Unlock()
if generationStartUnixTime == generationEndUnixTime {
if uint32(generationStartUnixTime) != uuidInfo.UnixTime {
mutex.Lock()
assert.Equal(t, uint32(generationStartUnixTime), uuidInfo.UnixTime)
mutex.Unlock()
}
}
if existedRoutineIndex, exists := generatedIds.Load(uuids[i]); exists {
mutex.Lock()
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, unix time is %d, seq id is %d, existed routine index is %d, current routine index is %d", uuids[i], uuidInfo.UnixTime, uuidInfo.SequentialId, existedRoutineIndex, currentRoutineIndex))
mutex.Unlock()
}
if uuidInfo.SequentialId == 0 {
fmt.Printf("routine#%d generate uuid %d\n", currentRoutineIndex, uuids[i])
generatedIds.Store(uuids[i], currentRoutineIndex)
if existedRoutineIndex, exists := generatedIds.Load(uuids[i]); exists {
mutex.Lock()
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, unix time is %d, seq id is %d, existed routine index is %d, current routine index is %d", uuids[i], uuidInfo.UnixTime, uuidInfo.SequentialId, existedRoutineIndex, currentRoutineIndex))
mutex.Unlock()
}
generatedIds.Store(uuids[i], currentRoutineIndex)
}
}
}
@@ -236,3 +321,78 @@ func TestGenerateUuids_20000TimesConcurrent(t *testing.T) {
waitGroup.Wait()
}
func TestGenerateUuids_1000000TimesConcurrent(t *testing.T) {
concurrentCount := 10
expectedUuidCount := uint8(250)
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 3})
var mutex sync.Mutex
var generatedIds sync.Map
var waitGroup sync.WaitGroup
for routineIndex := 0; routineIndex < concurrentCount; routineIndex++ {
waitGroup.Add(1)
go func(currentRoutineIndex int) {
for cycle := 0; cycle < 400; cycle++ {
if cycle%200 == 0 { // each server can only generate 500,000 (10 * 250 * 200) uuids in one second
time.Sleep(1000 * time.Millisecond)
}
generationStartUnixTime := time.Now().Unix()
uuids := generator.GenerateUuids(UUID_TYPE_USER, expectedUuidCount)
generationEndUnixTime := time.Now().Unix()
for i := 0; i < int(expectedUuidCount); i++ {
uuidInfo := generator.parseInternalUuidInfo(uuids[i])
if generationStartUnixTime == generationEndUnixTime {
if uint32(generationStartUnixTime) != uuidInfo.UnixTime {
mutex.Lock()
assert.Equal(t, uint32(generationStartUnixTime), uuidInfo.UnixTime)
mutex.Unlock()
}
}
if uuidInfo.SequentialId == 0 {
fmt.Printf("routine#%d generate uuid %d\n", currentRoutineIndex, uuids[i])
if existedRoutineIndex, exists := generatedIds.Load(uuids[i]); exists {
mutex.Lock()
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, unix time is %d, seq id is %d, existed routine index is %d, current routine index is %d", uuids[i], uuidInfo.UnixTime, uuidInfo.SequentialId, existedRoutineIndex, currentRoutineIndex))
mutex.Unlock()
}
generatedIds.Store(uuids[i], currentRoutineIndex)
}
}
}
waitGroup.Done()
}(routineIndex)
}
waitGroup.Wait()
}
func TestGenerateUuid_Over524287Times(t *testing.T) {
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 1})
onceGenerateCount := uint8(255)
generationStartUnixTime := time.Now().Unix()
for i := 0; i < 2057; i++ { // 2056*255=524280, 2057*255=524,535 (only can generates 524,287 uuids per second)
uuids := generator.GenerateUuids(UUID_TYPE_USER, onceGenerateCount)
if i < 2056 {
if len(uuids) < int(onceGenerateCount) {
assert.Fail(t, fmt.Sprintf("%d uuids should be generated", onceGenerateCount))
}
} else {
generationEndUnixTime := time.Now().Unix()
if generationStartUnixTime == generationEndUnixTime && len(uuids) > 0 {
assert.Fail(t, fmt.Sprintf("uuids should not be generated because there are too many uuids in one second"))
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<svg width="750" height="300" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<path d="m2.38122,215.61524l748.61878,-113.11523l0,197.99999l-750,0l1.38122,-84.88476z" fill="#111111" id="svg_3" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0" stroke-width="null"/>
<g id="BACKGROUND"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 449 B

+73
View File
@@ -0,0 +1,73 @@
<?xml version="1.0"?>
<svg width="750" height="300" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<path d="m2.381218,216.615238l748.618787,-113.11523l0,197.999993l-750.000002,0l1.381215,-84.884763z" fill="#f9f4ea" id="svg_3" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0" stroke-width="null"/>
<g id="BACKGROUND"/>
<g id="svg_1">
<path d="m623,300.642881c-63.833,0 -127.667,0 -191.5,0c0.109,-1.4 0.038,-2.844 0.365,-4.191c0.775,-3.196 1.715,-6.353 2.636,-9.511c0.602,-2.062 1.084,-4.195 1.979,-6.129c2.321,-5.011 3.543,-10.485 6.466,-15.235c0.126,-0.205 0.181,-0.471 0.207,-0.716c0.161,-1.56 1.197,-2.67 1.938,-3.947c1.587,-2.735 3.482,-5.229 5.48,-7.668c1.135,-1.386 2.09,-2.918 3.127,-4.385c0.878,0.766 1.792,1.494 2.627,2.303c3.598,3.486 7.44,6.677 11.787,9.17c6.411,3.678 13.171,5.353 20.6,3.493c4.237,-1.061 8.191,-2.676 12.041,-4.69c7.177,-3.755 13.324,-8.74 18.465,-14.989c0.938,-1.14 1.95,-2.218 2.932,-3.33c1.704,0.772 3.2,1.503 4.737,2.135c5.842,2.405 11.935,3.647 18.246,3.4c5.454,-0.214 10.695,-1.651 15.701,-3.762c10.855,-4.578 21.031,-10.344 30.219,-17.776c0.768,-0.622 1.628,-1.13 2.446,-1.692c2.325,-0.514 4.693,-0.884 6.966,-1.569c6.536,-1.969 12.432,-5.303 18.194,-8.877c2.296,-1.424 4.526,-2.953 6.786,-4.435c2.576,-0.065 5.17,-0.355 7.723,-0.145c4.5,0.369 8.904,1.307 12.737,3.908c2.238,1.519 3.939,3.496 4.736,6.04c-0.14,0.221 -0.167,0.323 -0.227,0.349c-1.063,0.465 -2.143,0.893 -3.194,1.383c-6.055,2.821 -10.479,7.337 -13.259,13.349c-2.424,5.241 -1.382,10.402 1.047,15.386c1.19,2.441 3.62,3.714 6.339,3.293c4.946,-0.766 8.461,-3.775 11.243,-7.64c3.751,-5.209 4.589,-11.135 3.601,-17.373c-0.335,-2.116 -0.785,-4.215 -1.232,-6.582c4.976,-0.537 9.83,-1.131 14.698,-1.569c6.83,-0.614 13.517,-1.964 19.931,-4.322c7.326,-2.694 14.531,-5.719 21.76,-8.672c7.81,-3.19 14.893,-7.747 22.164,-11.949c6.273,-3.625 11.789,-8.086 15.927,-14.114c0.129,-0.188 0.372,-0.296 0.562,-0.442c0,14.833 0,29.667 0,44.5c-2.472,0.148 -4.942,0.406 -7.415,0.423c-6.998,0.048 -13.85,1.277 -20.66,2.647c-6.182,1.243 -12.155,3.319 -18.031,5.599c-4.17,1.619 -8.171,3.69 -12.377,5.197c-6.222,2.23 -11.549,6.109 -17.404,9.01c-6.341,3.141 -11.549,7.948 -17.087,12.288c-6.142,4.813 -11.179,10.666 -16.643,16.128c-2.606,2.605 -4.496,5.696 -6.591,8.626c-3.688,5.156 -7.004,10.578 -10.453,15.904c-0.214,0.329 -0.232,0.783 -0.34,1.179z" fill="#FEDBD0" id="svg_4"/>
<path d="m623,300.642881c0.108,-0.396 0.126,-0.85 0.338,-1.178c3.449,-5.325 6.766,-10.747 10.453,-15.904c2.095,-2.93 3.985,-6.021 6.591,-8.626c5.464,-5.462 10.501,-11.315 16.643,-16.128c5.538,-4.34 10.746,-9.147 17.087,-12.288c5.855,-2.901 11.182,-6.78 17.404,-9.01c4.206,-1.507 8.207,-3.578 12.377,-5.197c5.876,-2.281 11.849,-4.356 18.031,-5.599c6.81,-1.37 13.662,-2.598 20.66,-2.647c2.472,-0.017 4.943,-0.275 7.415,-0.423c0,25.667 0,51.333 0,77c-42.332,0 -84.666,0 -126.999,0zm53.375,-36.965c1.139,-0.251 2.619,-0.443 3.985,-0.943c0.563,-0.206 1.229,-1.084 1.215,-1.641c-0.013,-0.519 -0.836,-1.415 -1.355,-1.454c-2.359,-0.18 -4.741,-0.217 -7.104,-0.089c-0.962,0.052 -1.502,0.973 -1.518,1.987c-0.016,0.99 0.656,1.517 1.505,1.71c0.952,0.218 1.942,0.265 3.272,0.43zm31.44,-5.633c-1.287,0.123 -2.369,0.146 -3.416,0.349c-0.971,0.189 -1.705,0.856 -1.602,1.897c0.107,1.09 0.798,1.832 2.005,1.764c2.388,-0.135 4.776,-0.288 7.159,-0.498c0.801,-0.071 1.394,-0.547 1.424,-1.445c0.031,-0.915 -0.549,-1.353 -1.359,-1.482c-1.467,-0.232 -2.94,-0.411 -4.211,-0.585zm18.439,-17.441c1.112,-0.215 2.701,-0.371 4.167,-0.88c0.615,-0.213 1.278,-1.134 1.369,-1.807c0.134,-0.982 -0.882,-1.442 -1.701,-1.451c-2.232,-0.024 -4.478,0.013 -6.689,0.279c-0.592,0.071 -1.399,0.978 -1.532,1.621c-0.188,0.906 0.367,1.814 1.446,2.009c0.808,0.145 1.643,0.135 2.94,0.229zm-28.051,11.011c0.01,0.05 0.02,0.1 0.03,0.149c1.375,-0.248 2.772,-0.412 4.117,-0.773c0.882,-0.237 1.453,-1.041 1.088,-1.923c-0.234,-0.566 -0.959,-1.235 -1.526,-1.297c-1.551,-0.169 -3.138,-0.088 -4.705,-0.005c-0.552,0.029 -1.29,0.196 -1.585,0.577c-0.46,0.595 -0.888,1.461 -0.802,2.145c0.056,0.438 1.022,0.872 1.655,1.091c0.52,0.178 1.149,0.036 1.728,0.036zm16.873,29.366c0.595,-0.074 1.654,-0.193 2.711,-0.339c1.004,-0.139 1.888,-0.673 1.714,-1.707c-0.109,-0.646 -0.848,-1.376 -1.491,-1.706c-1.902,-0.976 -3.9,-0.309 -5.789,0.143c-0.519,0.124 -1.159,1.292 -1.087,1.904c0.073,0.618 0.87,1.313 1.518,1.651c0.518,0.27 1.289,0.054 2.424,0.054zm-62.107,-1.702c0.992,-0.141 2.333,-0.217 3.6,-0.565c0.519,-0.142 1.203,-0.862 1.214,-1.333c0.01,-0.437 -0.739,-1.228 -1.219,-1.282c-2.214,-0.249 -4.455,-0.413 -6.676,-0.329c-0.544,0.02 -1.336,1.018 -1.5,1.692c-0.23,0.941 0.638,1.49 1.508,1.636c0.896,0.15 1.823,0.117 3.073,0.181zm56.714,10.81c-0.95,0.101 -1.623,0.107 -2.263,0.255c-0.974,0.227 -1.67,0.848 -1.699,1.888c-0.03,1.088 0.736,1.636 1.698,1.861c1.885,0.441 3.648,-0.083 5.244,-1.002c0.951,-0.548 0.863,-1.725 -0.147,-2.201c-0.951,-0.447 -2.046,-0.59 -2.833,-0.801zm26.315,-37.502c0.766,-0.263 1.911,-0.507 2.907,-1.034c1.329,-0.703 1.286,-2.119 -0.137,-2.548c-1.368,-0.413 -2.888,-0.43 -4.34,-0.418c-1.058,0.008 -1.957,0.585 -1.937,1.817c0.019,1.175 0.899,1.694 1.904,1.985c0.389,0.112 0.813,0.105 1.603,0.198zm0.345,19.21c0.952,-0.19 1.982,-0.219 2.829,-0.627c0.533,-0.258 1.055,-1.068 1.089,-1.659c0.026,-0.456 -0.629,-1.207 -1.134,-1.38c-0.902,-0.31 -1.919,-0.375 -2.888,-0.376c-2.054,-0.003 -3.157,0.781 -3.079,2.046c0.086,1.385 0.91,1.871 3.183,1.996zm-51.014,9.677c1.121,-0.151 2.125,-0.188 3.072,-0.44c1.06,-0.282 1.275,-1.309 0.548,-2.125c-0.839,-0.942 -4.41,-1.793 -5.578,-1.336c-0.85,0.332 -1.374,0.897 -1.335,1.857c0.039,0.958 0.583,1.513 1.482,1.72c0.639,0.147 1.292,0.233 1.811,0.324zm54.676,11.76c-1.539,-1.969 -3.594,-2.067 -5.636,-1.96c-0.518,0.027 -1.346,0.675 -1.414,1.13c-0.09,0.604 0.254,1.474 0.712,1.921c1.518,1.481 4.853,0.946 6.338,-1.091zm-63.279,1.341c0.587,-0.147 1.677,-0.286 2.637,-0.716c0.486,-0.218 1.047,-0.983 1.011,-1.458c-0.039,-0.507 -0.658,-1.215 -1.173,-1.388c-0.918,-0.307 -1.947,-0.334 -2.934,-0.373c-1.421,-0.057 -2.583,0.753 -2.604,1.715c-0.021,1.006 1.352,2.18 3.063,2.22zm-31.358,-4.32c-0.518,0.079 -1.188,0.107 -1.81,0.293c-0.819,0.245 -1.502,0.673 -1.465,1.719c0.041,1.148 0.804,1.672 1.744,1.718c1.049,0.052 2.16,-0.065 3.152,-0.39c0.58,-0.19 1.252,-0.858 1.4,-1.436c0.245,-0.953 -0.599,-1.458 -1.413,-1.727c-0.457,-0.149 -0.975,-0.114 -1.608,-0.177z" fill="#548099" id="svg_7"/>
<path d="m346.953,290.630881c0.806,1.415 1.73,2.778 2.39,4.258c0.832,1.868 1.448,3.832 2.157,5.754c-43.5,0 -87,0 -130.5,0c0.664,-4.841 1.467,-9.669 1.949,-14.528c0.409,-4.123 0.447,-8.283 0.649,-12.426c0.655,-0.292 1.496,-0.416 1.932,-0.903c1.935,-2.161 4.26,-3.808 6.56,-5.537c3.753,-2.82 7.508,-5.604 11.694,-7.767c0.521,-0.269 0.858,-0.895 1.28,-1.356c1.969,-0.804 3.904,-1.705 5.914,-2.389c2.802,-0.953 5.66,-1.745 8.494,-2.605c0.444,0.781 0.854,1.584 1.34,2.338c1.034,1.602 3.777,1.039 4.439,-0.312c0.597,-1.219 1.348,-2.508 0.685,-3.985c1.22,-0.01 2.44,-0.019 3.659,-0.029c0.702,1.529 1.924,2.014 3.551,1.953c1.667,-0.062 1.744,-1.442 2.288,-2.46c1.394,-0.16 2.786,-0.341 4.183,-0.474c0.906,-0.086 1.406,0.366 1.468,1.282c-0.581,0.457 -1.236,0.848 -1.725,1.388c-0.997,1.1 -1.005,2.435 -0.49,3.73c0.443,1.113 1.448,1.554 2.592,1.627c1.41,0.09 3.026,-0.987 3.526,-2.301c0.455,-1.196 0.04,-2.505 -1.157,-3.529c-0.436,-0.373 -0.922,-0.689 -1.385,-1.031c0.254,-0.166 0.511,-0.479 0.762,-0.474c1.561,0.031 3.12,0.133 4.678,0.211c1.094,0.858 2.293,1.005 3.583,0.518c0.516,0.348 0.986,0.863 1.554,1.015c2.989,0.797 5.976,1.653 9.016,2.189c2.214,0.391 4.217,1.307 6.306,1.99c2.187,0.715 4.316,1.674 6.363,2.734c2.392,1.238 4.669,2.697 6.993,4.066c3.399,2.002 6.29,4.673 9.358,7.11c-0.825,0.879 -1.67,1.742 -2.471,2.642c-2.811,3.157 -5.61,6.324 -8.407,9.493c-0.931,1.055 -1.882,2.098 -2.752,3.203c-1.282,1.629 -1.073,4.988 0.312,5.917c1.948,1.307 4.248,0.857 5.65,-1.118c0.623,-0.878 1.155,-1.829 1.845,-2.649c3.565,-4.239 7.427,-8.242 10.273,-13.047c2.732,1.785 4.457,4.533 6.55,6.923c0.331,0.378 0.315,1.06 0.46,1.601c-0.496,0.652 -1.051,1.268 -1.474,1.965c-0.803,1.321 -0.775,3.932 -0.021,4.694c1.143,1.154 3.035,1.425 4.766,0.701c0.375,-0.158 0.775,-0.257 1.163,-0.382zm-72.407,-25.96c0.019,-2.027 -1.097,-3.298 -2.884,-3.284c-1.897,0.015 -4.097,2.687 -4.097,4.975c0,1.415 1.258,2.597 2.755,2.588c2.478,-0.015 4.202,-1.761 4.226,-4.279zm25.954,-5.911c0.013,-1.961 -1.734,-3.932 -3.438,-3.88c-1.956,0.061 -3.375,1.636 -3.444,3.82c-0.054,1.74 1.356,2.824 3.628,2.788c1.744,-0.026 3.244,-1.285 3.254,-2.728zm-11.518,6.038c0.038,-2.048 -0.319,-2.983 -1.43,-3.368c-1.198,-0.415 -2.281,-0.054 -3.198,0.77c-1.084,0.975 -1.515,2.591 -1.055,3.791c0.59,1.539 1.254,2.056 2.702,2.102c1.99,0.063 2.939,-0.986 2.981,-3.295z" fill="#DAF1FF" id="svg_9"/>
<path d="m703.429,159.265881c0.502,-1.426 1.928,-1.133 2.946,-1.451c1.315,-0.41 2.746,-0.447 4.128,-0.646c-0.073,1.037 -0.284,2.089 -0.18,3.108c0.102,0.994 0.945,1.465 1.891,1.687c1.031,0.242 1.717,-0.193 2.278,-1.028c0.839,-1.249 1.542,-2.52 1.195,-4.107c2.698,-0.336 5.388,-0.789 8.096,-0.966c1.798,-0.118 3.675,0.481 5.423,0.193c4.629,-0.762 9.263,-0.294 13.889,-0.384c2.297,-0.044 4.602,0.301 6.904,0.47c0,6 0,12 0,18c-0.168,0.168 -0.379,0.31 -0.499,0.507c-3.606,5.916 -8.197,10.898 -14.1,14.566c-9.541,5.927 -19.201,11.641 -29.712,15.749c-4.875,1.905 -9.688,3.974 -14.602,5.768c-6.175,2.255 -12.495,3.985 -19.078,4.65c-6.024,0.609 -12.036,1.349 -18.086,2.036c-0.09,-0.077 -0.245,-0.163 -0.332,-0.294c-0.23,-0.345 -0.451,-0.7 -0.642,-1.069c-2.945,-5.662 -7.74,-8.766 -13.872,-9.97c-2.517,-0.494 -5.059,-0.863 -7.59,-1.289c0.122,-1.386 1.372,-1.852 2.257,-2.453c3.107,-2.111 5.83,-4.582 8.397,-7.337c2.64,-2.832 5.637,-5.351 8.608,-7.852c3.084,-2.596 6.282,-5.065 9.508,-7.485c4.242,-3.181 8.981,-5.539 13.649,-8.011c1.312,-0.695 2.5,-1.622 3.798,-2.346c2.435,-1.357 4.861,-2.746 7.38,-3.931c3.057,-1.438 5.85,-3.482 9.246,-4.157c1.207,-0.24 2.427,-0.418 3.641,-0.625c0.038,0.747 0.084,1.493 0.112,2.241c0.048,1.279 0.794,1.688 1.971,1.714c3.458,0.077 4.892,-2.151 3.376,-5.288z" fill="#FEDBD0" id="svg_10"/>
<path d="m57.51,258.254881c7.145,-2.064 14.546,-3.315 21.271,-6.71c0.294,-0.148 0.596,-0.312 0.913,-0.376c1.115,-0.226 1.846,0.461 1.457,1.539c-0.502,1.392 -1.12,2.772 -1.899,4.026c-1.746,2.811 -3.623,5.542 -5.446,8.305c-3.48,3.206 -6.708,6.758 -10.503,9.534c-5.403,3.951 -11.477,6.726 -18.266,7.474c-3.769,0.415 -7.528,1.343 -11.356,0.469c-0.543,-0.124 -1.098,-0.198 -1.702,-0.304c-2.741,5.966 -4.551,12.065 -5.48,18.43c-0.667,0 -1.333,0 -2,0c0.047,-0.996 -0.041,-2.022 0.163,-2.985c1.855,-8.751 4.84,-17.099 9.313,-24.86c2.197,-3.813 4.602,-7.509 7.008,-11.196c1.63,-2.497 3.424,-4.887 5.145,-7.325c0.707,-0.793 1.414,-1.587 1.826,-2.049c-1.337,-4.444 -2.733,-8.452 -3.722,-12.558c-0.906,-3.763 -0.302,-7.588 0.281,-11.383c0.427,-2.786 0.691,-5.602 0.898,-8.414c0.539,-7.303 1.03,-14.61 1.491,-21.919c0.141,-2.242 0.072,-4.498 0.198,-6.741c0.083,-1.468 -0.072,-3.421 1.695,-3.792c1.799,-0.378 2.727,1.386 3.382,2.777c1.878,3.985 3.742,7.986 4.607,12.361c0.5,2.527 1.048,5.048 1.682,7.544c1.66,6.54 1.353,13.035 -0.341,19.504c-1.01,3.857 -2.083,7.696 -3.115,11.547c-0.081,0.302 -0.059,0.632 -0.138,1.602c4.488,-4.461 7.787,-9.186 11.478,-13.553c3.756,-4.445 7.3,-9.081 10.746,-13.773c3.441,-4.686 6.087,-9.859 8.756,-15.053c-1.95,-0.825 -3.721,-1.493 -5.418,-2.314c-1.995,-0.966 -3.474,-2.515 -4.659,-4.396c-2.918,-4.634 -4.822,-9.683 -6.084,-14.97c-1.099,-4.607 -2.018,-9.257 -2.983,-13.895c-0.37,-1.781 0.934,-2.944 2.664,-2.336c0.675,0.237 1.307,0.735 1.828,1.246c5.293,5.196 10.216,10.715 14.295,16.93c0.951,1.448 1.61,3.09 2.377,4.656c0.715,1.462 1.396,2.941 2.091,4.412c0.184,-0.025 0.369,-0.049 0.553,-0.074c0.57,-2.137 1.031,-4.311 1.741,-6.4c0.686,-2.019 0.513,-3.751 -0.461,-5.692c-1.933,-3.85 -2.263,-8.14 -2.788,-12.352c-0.267,-2.142 -0.713,-4.277 -0.791,-6.425c-0.243,-6.719 2.212,-12.722 5.395,-18.432c1.78,-3.192 3.947,-6.169 5.951,-9.235c0.226,-0.346 0.484,-0.688 0.789,-0.962c1.019,-0.913 1.987,-0.691 2.548,0.593c0.197,0.453 0.33,0.948 0.404,1.438c0.771,5.099 1.548,10.196 1.283,15.386c-0.242,4.729 -0.409,9.461 -0.621,14.191c-0.263,5.86 -1.892,11.295 -5.25,16.133c-0.69,0.994 -1.642,1.891 -2.657,2.548c-0.904,0.586 -1.382,1.11 -1.026,2.166c0.173,0.034 0.378,0.141 0.461,0.081c4.238,-3.071 9.005,-4.968 13.999,-6.39c3.119,-0.888 6.217,-1.852 9.316,-2.809c0.709,-0.219 1.533,-0.754 1.974,0.12c0.211,0.418 -0.073,1.317 -0.429,1.756c-3.838,4.724 -7.825,9.269 -13.147,12.472c-4.119,2.479 -8.495,3.238 -13.181,2.755c-0.401,-0.041 -0.801,-0.094 -1.403,-0.165c-0.513,1.27 -1.061,2.461 -1.48,3.695c-1.126,3.313 -2.51,6.49 -4.453,9.422c-0.589,0.889 -0.936,1.94 -1.393,2.917c-0.092,0.177 -0.184,0.354 -0.276,0.531c0.237,-0.025 0.475,-0.049 0.712,-0.074c8.518,-4.686 17.852,-5.489 27.302,-5.726c4.238,-0.106 8.496,0.056 12.66,-0.997c0.32,-0.081 0.688,-0.208 0.977,-0.122c0.431,0.129 0.934,0.345 1.165,0.687c0.135,0.199 -0.127,0.769 -0.346,1.076c-0.233,0.327 -0.62,0.546 -0.949,0.8c-4.471,3.456 -8.851,7.037 -13.446,10.319c-3.093,2.209 -6.334,4.341 -9.788,5.883c-4.479,1.999 -9.254,2.422 -14.099,0.841c-3.231,-1.054 -6.577,-1.775 -9.11,-4.412c-5.55,7.005 -10.925,13.789 -16.3,20.573c0.1,0.131 0.201,0.262 0.301,0.394c1.158,-0.542 2.28,-1.184 3.48,-1.603c2.105,-0.736 4.22,-1.533 6.399,-1.952c4.669,-0.897 9.384,-1.167 14.146,-0.602c2.304,0.273 4.653,0.157 6.981,0.233c0.495,0.016 1.018,0.013 1.476,0.172c0.931,0.323 1.159,1.218 0.56,1.997c-0.15,0.194 -0.348,0.364 -0.553,0.501c-5.39,3.602 -10.583,7.572 -16.826,9.66c-4.521,1.513 -9.105,2.594 -13.959,1.56c-2.984,-0.636 -6.028,-0.992 -9.407,-1.532c-1.229,1.622 -2.828,3.432 -4.06,5.465c-0.787,1.297 -1.522,2.517 -2.777,3.402c-0.313,0.22 -0.452,0.686 -0.672,1.038c-2.79,3.854 -5.6,7.693 -8.352,11.574c-0.505,0.712 -0.774,1.592 -1.151,2.396c-0.148,0.187 -0.298,0.372 -0.446,0.558c-0.049,0.137 -0.098,0.273 -0.148,0.409c0.085,-0.101 0.169,-0.202 0.254,-0.303c0.175,-0.159 0.349,-0.318 0.524,-0.476c0.359,-0.195 0.757,-0.343 1.072,-0.594c4.498,-3.578 9.142,-6.901 14.687,-8.726c0.769,-0.253 1.457,-0.754 2.182,-1.141zm-3.085,-15.668c-0.545,0.543 -0.931,1.13 -0.546,2.46c0.526,-0.893 0.821,-1.396 1.117,-1.898c-0.19,-0.188 -0.381,-0.375 -0.571,-0.562z" fill="#56819B" id="svg_11"/>
<path d="m203.5,300.642881c-28.167,0 -56.333,0 -84.5,0c1.579,-2.025 3.197,-4.021 4.731,-6.079c6.246,-8.378 11.41,-17.326 14.302,-27.453c0.29,-1.015 0.785,-1.708 1.872,-2.113c1.625,-0.605 3.261,-1.3 4.707,-2.241c2.703,-1.757 5.273,-3.719 7.899,-5.595c0.724,0.841 1.492,0.323 2.277,0.105c1.509,-0.419 3.015,-0.961 4.557,-1.123c1.518,-0.159 2.939,-0.342 4.212,-1.261c0.46,-0.332 1.027,-0.601 1.581,-0.702c3.082,-0.565 6.171,-1.099 9.267,-1.587c3.507,-0.553 7.013,-1.126 10.537,-1.545c4.593,-0.547 9.198,-1.015 13.808,-1.394c3.384,-0.278 6.784,-0.345 10.175,-0.546c0.372,-0.022 0.727,-0.309 1.091,-0.473c0.773,0.86 1.621,1.664 2.303,2.59c1.619,2.199 3.404,4.325 4.683,6.715c3.164,5.915 4.317,12.338 4.11,19.02c-1.822,1.284 -3.545,2.656 -4.742,4.6c-0.692,1.125 -1.486,2.192 -2.282,3.248c-1.578,2.094 -3.217,4.141 -4.778,6.247c-1.37,1.847 -2.75,3.695 -3.969,5.642c-0.765,1.221 -1.238,2.624 -1.841,3.945z" fill="#B2CDDD" id="svg_12"/>
<path d="m0,167.642881c1.953,0.858 4.023,1.524 5.837,2.613c4.339,2.606 8.422,5.588 12.183,9.001c7.607,6.902 12.849,15.19 15.549,25.179c1.135,4.2 1.898,8.407 2.177,12.703c0.491,7.553 -0.901,14.816 -3.643,21.862c-1.81,4.652 -4.023,9.07 -6.872,13.17c-0.178,0.257 -0.213,0.613 -0.314,0.923c-0.706,0.697 -1.407,1.399 -2.12,2.089c-1.599,1.548 -3.107,3.208 -4.825,4.612c-4.805,3.927 -9.685,7.762 -14.564,11.598c-1.008,0.792 -1.893,1.912 -3.409,1.75c0.001,-35.167 0.001,-70.333 0.001,-105.5z" fill="#54809A" id="svg_14"/>
<path d="m26.5,300.642881c0.929,-6.365 2.74,-12.464 5.48,-18.43c0.603,0.106 1.158,0.18 1.702,0.304c3.828,0.874 7.586,-0.054 11.356,-0.469c6.789,-0.748 12.863,-3.524 18.266,-7.474c3.796,-2.775 7.023,-6.328 10.503,-9.534c3.102,2.025 6.632,2.88 10.186,3.581c4.458,0.88 8.946,1.606 13.421,2.397c0.163,0.029 0.331,0.078 0.493,0.066c5.706,-0.412 11.501,0.316 17.092,-1.612c2.083,-0.719 4.354,-0.881 6.519,-1.383c0.977,-0.227 2.248,-0.094 2.553,-1.51c3.712,1.091 7.464,0.778 11.605,0.016c-0.403,1.373 -0.662,2.386 -0.997,3.374c-3.081,9.089 -8.229,17.02 -13.851,24.685c-1.456,1.986 -2.886,3.991 -4.328,5.988c-30,0.001 -60,0.001 -90,0.001z" fill="#B2CDDD" id="svg_15"/>
<path d="m453.697,248.860881c-1.036,1.466 -1.992,2.999 -3.127,4.385c-1.997,2.439 -3.893,4.933 -5.48,7.668c-0.741,1.277 -1.777,2.387 -1.938,3.947c-0.025,0.244 -0.081,0.511 -0.207,0.716c-2.923,4.751 -4.145,10.224 -6.466,15.235c-0.896,1.934 -1.377,4.067 -1.979,6.129c-0.922,3.158 -1.861,6.316 -2.636,9.511c-0.327,1.347 -0.255,2.791 -0.365,4.191c-8.333,0 -16.667,0 -25,0c-0.064,-0.492 -0.127,-0.984 -0.191,-1.476c-0.786,-6.001 -1.833,-11.982 -2.289,-18.008c-0.5,-6.618 -0.296,-13.268 0.94,-19.837c1.29,-6.857 3.538,-13.302 7.994,-18.814c1.804,-2.231 3.905,-4.056 6.617,-5.126c7.006,-2.763 13.85,-2.131 20.494,1.109c5.215,2.544 9.46,6.425 13.633,10.37z" fill="#FEF6F3" id="svg_18"/>
<path d="m453.697,248.860881c-4.173,-3.945 -8.418,-7.826 -13.632,-10.369c-6.644,-3.241 -13.488,-3.872 -20.494,-1.109c-2.712,1.069 -4.814,2.894 -6.617,5.126c-4.456,5.512 -6.704,11.957 -7.994,18.814c-1.236,6.569 -1.44,13.219 -0.94,19.837c0.456,6.026 1.503,12.007 2.289,18.008c0.064,0.492 0.128,0.984 0.191,1.476c-1,0 -2,0 -3,0c-0.048,-0.899 -0.047,-1.804 -0.152,-2.696c-0.636,-5.436 -1.441,-10.857 -1.901,-16.307c-0.335,-3.965 -0.414,-7.975 -0.272,-11.952c0.276,-7.714 1.882,-15.169 5.327,-22.127c2.157,-4.356 4.886,-8.252 9.027,-11.079c4.653,-3.177 9.791,-4.007 15.208,-3.599c7.091,0.533 12.977,3.859 18.324,8.305c2.236,1.859 4.459,3.733 6.689,5.599c3.79,3.227 7.337,6.807 11.421,9.605c7.432,5.091 15.641,6.85 24.394,3.67c4.21,-1.529 8.286,-3.37 12.08,-5.845c5.415,-3.533 10.19,-7.729 14.251,-12.749c0.603,-0.745 1.155,-1.531 1.799,-2.388c-4.865,-3.29 -8.586,-7.267 -11.563,-11.948c-1.977,-4.036 -3.754,-8.144 -4.246,-12.674c-0.618,-5.684 0.519,-10.88 4.306,-15.343c7.45,-8.777 17.131,-4.118 20.673,1.989c3.019,5.207 4.006,10.856 2.952,16.788c-0.522,2.935 -1.291,5.827 -1.948,8.738c-1.193,2.637 -2.33,5.303 -3.604,7.9c-0.639,1.302 -1.527,2.482 -2.352,3.795c1.172,0.621 2.086,1.221 3.08,1.615c10.465,4.143 20.984,4.634 31.595,0.488c5.979,-2.336 11.681,-5.231 17.151,-8.551c3.608,-2.19 6.999,-4.739 10.487,-7.127c2.397,-1.768 4.815,-3.507 7.186,-5.31c7.254,-5.519 15.14,-9.814 23.882,-12.49c3.984,-1.219 8.052,-1.862 12.21,-1.993c0.661,-0.021 1.321,-0.104 1.981,-0.157c2.531,0.427 5.073,0.795 7.59,1.29c6.133,1.204 10.928,4.308 13.872,9.97c0.191,0.368 0.411,0.723 0.642,1.069c0.087,0.131 0.242,0.217 0.332,0.294c6.05,-0.686 12.062,-1.426 18.086,-2.036c6.583,-0.666 12.902,-2.396 19.078,-4.65c4.914,-1.794 9.728,-3.863 14.602,-5.768c10.511,-4.108 20.171,-9.822 29.712,-15.749c5.903,-3.667 10.494,-8.65 14.1,-14.566c0.12,-0.197 0.331,-0.339 0.499,-0.507c0,1.667 0,3.333 0,5c-0.19,0.146 -0.434,0.254 -0.562,0.442c-4.137,6.029 -9.654,10.489 -15.927,14.114c-7.271,4.202 -14.354,8.758 -22.164,11.949c-7.229,2.953 -14.434,5.979 -21.76,8.672c-6.414,2.358 -13.101,3.708 -19.931,4.322c-4.868,0.438 -9.722,1.032 -14.698,1.569c0.447,2.367 0.896,4.466 1.232,6.582c0.989,6.238 0.15,12.164 -3.601,17.373c-2.782,3.864 -6.297,6.874 -11.243,7.64c-2.72,0.421 -5.149,-0.852 -6.339,-3.293c-2.429,-4.984 -3.471,-10.145 -1.047,-15.386c2.78,-6.012 7.204,-10.527 13.259,-13.349c1.051,-0.49 2.131,-0.918 3.194,-1.383c0.06,-0.026 0.087,-0.129 0.227,-0.349c-0.797,-2.545 -2.498,-4.521 -4.736,-6.04c-3.833,-2.602 -8.237,-3.539 -12.737,-3.908c-2.553,-0.209 -5.147,0.08 -7.722,0.145c-2.839,0.556 -5.759,0.858 -8.5,1.723c-5.473,1.727 -10.671,4.173 -15.496,7.29c-2.762,1.783 -5.307,3.901 -7.95,5.869c-0.817,0.561 -1.677,1.07 -2.446,1.692c-9.188,7.432 -19.365,13.198 -30.219,17.776c-5.006,2.111 -10.247,3.548 -15.701,3.762c-6.311,0.247 -12.404,-0.995 -18.246,-3.4c-1.536,-0.632 -3.032,-1.364 -4.737,-2.135c-0.982,1.112 -1.995,2.19 -2.932,3.33c-5.141,6.249 -11.288,11.234 -18.465,14.989c-3.85,2.014 -7.804,3.629 -12.041,4.69c-7.43,1.86 -14.189,0.184 -20.6,-3.493c-4.347,-2.493 -8.189,-5.684 -11.787,-9.17c-0.834,-0.816 -1.748,-1.545 -2.626,-2.31zm73.068,-22.082c1.719,-5.217 2.952,-10.54 2.417,-16.06c-0.418,-4.322 -1.932,-8.262 -5.474,-11.091c-3.886,-3.104 -8.374,-3.952 -12.441,0.361c-3.003,3.185 -4.691,7.101 -4.647,11.502c0.057,5.679 1.696,10.943 5.001,15.629c2.474,4.007 5.76,7.16 9.901,9.719c2.297,-3.261 4.164,-6.449 5.243,-10.06zm125.196,-5.674c-5.95,1.56 -10.071,5.141 -13.571,9.623c-2.34,2.996 -2.873,6.522 -2.83,10.114c0.026,2.222 0.639,4.441 1.956,6.347c1.279,1.852 2.618,2.387 4.71,1.598c1.375,-0.518 2.773,-1.183 3.906,-2.097c6.705,-5.413 8.435,-12.613 7.132,-20.786c-0.256,-1.601 -0.846,-3.15 -1.303,-4.799z" fill="#5D859D" id="svg_20"/>
<path d="m0,273.142881c1.516,0.161 2.401,-0.958 3.409,-1.75c4.879,-3.836 9.759,-7.671 14.564,-11.598c1.717,-1.404 3.226,-3.064 4.825,-4.612c0.713,-0.69 1.414,-1.392 2.12,-2.089c0.999,0.956 2.174,0.507 3.297,0.397c2.798,-0.273 5.591,-0.608 8.392,-0.847c3.306,-0.282 6.367,0.969 9.522,1.631c-1.72,2.438 -3.515,4.828 -5.144,7.325c-2.406,3.687 -4.811,7.384 -7.008,11.196c-4.473,7.761 -7.457,16.109 -9.313,24.86c-0.204,0.963 -0.116,1.988 -0.163,2.985c-8.167,0 -16.333,0 -24.5,0c-0.001,-9.165 -0.001,-18.331 -0.001,-27.498z" fill="#B2CDDD" id="svg_24"/>
<path d="m116.5,300.642881c1.442,-1.996 2.872,-4.002 4.328,-5.988c5.622,-7.665 10.77,-15.596 13.851,-24.685c0.335,-0.988 0.594,-2.002 0.997,-3.374c-4.141,0.762 -7.892,1.075 -11.605,-0.016c-2.444,-2.027 -4.795,-4.112 -5.824,-7.288c-2.189,-6.757 -0.875,-12.83 3.686,-18.161c2.091,-2.444 5.436,-2.988 8.48,-1.804c4.269,1.661 6.988,4.729 8.105,9.135c0.943,3.719 1.461,7.501 0.999,11.362c-0.087,0.726 -0.013,1.471 -0.013,2.42c0.622,-0.123 1.191,-0.128 1.66,-0.349c1.127,-0.53 2.259,-1.079 3.295,-1.764c5.63,-3.723 11.032,-7.753 15.879,-12.467c8.534,-8.302 18.932,-11.766 30.552,-10.834c8.313,0.666 15.79,3.901 21.214,10.745c0.508,0.641 1.185,1.147 1.784,1.715c1.658,2.364 3.474,4.635 4.939,7.113c3.137,5.308 4.259,11.225 4.77,17.286c-0.202,4.143 -0.24,8.303 -0.649,12.426c-0.482,4.859 -1.285,9.687 -1.949,14.528c-0.667,0 -1.333,0 -2,0c0.064,-0.902 0.077,-1.811 0.199,-2.704c0.954,-6.964 1.934,-13.924 1.911,-20.977c0.208,-6.682 -0.946,-13.105 -4.11,-19.02c-1.279,-2.39 -3.064,-4.516 -4.683,-6.715c-0.682,-0.926 -1.53,-1.731 -2.303,-2.59c-1.733,-2.164 -3.651,-4.181 -6.227,-5.273c-3.279,-1.39 -6.602,-2.749 -10.019,-3.724c-4.067,-1.161 -8.247,-0.643 -12.392,-0.074c-6.704,0.92 -12.55,3.703 -17.688,8.031c-3.75,3.16 -7.454,6.375 -11.178,9.566c-2.626,1.876 -5.196,3.838 -7.899,5.595c-1.447,0.941 -3.082,1.635 -4.707,2.241c-1.087,0.405 -1.582,1.099 -1.872,2.113c-2.892,10.127 -8.056,19.075 -14.302,27.453c-1.535,2.058 -3.152,4.055 -4.731,6.079c-0.831,-0.001 -1.665,-0.001 -2.498,-0.001zm21.085,-44.076c-0.226,-1.841 -0.346,-3.67 -0.69,-5.455c-0.702,-3.64 -2.383,-6.701 -5.587,-8.784c-3.248,-2.112 -6.703,-1.67 -8.715,1.513c-2.711,4.287 -3.974,9.005 -2.481,14.042c1.149,3.876 3.627,6.344 7.888,6.96c2.418,0.349 4.596,-0.35 6.873,-0.68c1.263,-0.183 1.866,-0.953 2.018,-2.163c0.228,-1.812 0.463,-3.622 0.694,-5.433z" fill="#5E879F" id="svg_33"/>
<path d="m221.11,276.961881c0.023,7.053 -0.957,14.013 -1.911,20.977c-0.123,0.894 -0.136,1.803 -0.199,2.704c-5.167,0 -10.333,0 -15.5,0c0.603,-1.321 1.076,-2.724 1.839,-3.944c1.218,-1.946 2.599,-3.795 3.969,-5.642c1.561,-2.105 3.2,-4.153 4.778,-6.247c0.796,-1.057 1.59,-2.123 2.282,-3.248c1.197,-1.945 2.92,-3.316 4.742,-4.6z" fill="#DAF1FF" id="svg_34"/>
<path d="m508.133,227.129881c2.977,4.682 6.698,8.658 11.563,11.948c-0.643,0.857 -1.196,1.643 -1.799,2.388c-4.06,5.019 -8.836,9.216 -14.251,12.749c-3.794,2.475 -7.869,4.315 -12.08,5.845c-8.754,3.18 -16.962,1.421 -24.394,-3.67c-4.084,-2.797 -7.631,-6.378 -11.421,-9.605c0.51,-0.232 1.208,-0.327 1.499,-0.721c1.406,-1.904 3.562,-2.659 5.458,-3.826c0.553,-0.341 0.885,-1.041 1.319,-1.576c2.188,-1.133 4.354,-2.31 6.567,-3.391c5.425,-2.651 10.837,-5.319 16.666,-7.044c1.654,-0.49 3.333,-0.605 4.974,-0.974c5.244,-1.179 10.582,-1.552 15.899,-2.123z" fill="#FEDBD0" id="svg_39"/>
<path d="m586.228,224.746881c-3.488,2.388 -6.878,4.937 -10.487,7.127c-5.47,3.32 -11.172,6.215 -17.151,8.551c-10.612,4.146 -21.13,3.656 -31.595,-0.488c-0.995,-0.394 -1.908,-0.993 -3.08,-1.615c0.824,-1.313 1.713,-2.492 2.352,-3.795c1.274,-2.598 2.411,-5.263 3.604,-7.9c5.575,0.006 11.151,0.061 16.726,-0.003c2.524,-0.029 5.044,-0.372 7.568,-0.427c1.561,-0.034 3.128,0.21 4.693,0.321c0.579,0.041 1.161,0.112 1.738,0.083c2.645,-0.133 5.286,-0.381 7.931,-0.43c5.194,-0.097 10.349,-0.52 15.467,-1.418c0.723,-0.127 1.488,-0.01 2.234,-0.006z" fill="#FEDAD0" id="svg_40"/>
<path d="m595.508,185.574881c1.815,-0.428 3.265,-0.768 4.714,-1.114c1.426,-0.341 2.629,-0.045 3.518,1.197c0.932,1.301 0.614,2.648 -0.249,3.691c-1.258,1.521 -2.76,2.843 -4.187,4.221c-0.475,0.459 -1.03,0.837 -1.562,1.234c-0.678,0.505 -1.005,1.152 -0.635,1.948c0.77,1.659 0.227,2.937 -1.15,3.892c-1.168,0.81 -2.502,0.376 -4.104,-1.156c-0.049,-0.047 -0.145,-0.046 -0.414,-0.123c-1.114,0.772 -2.349,1.596 -3.549,2.469c-1.042,0.758 -2.035,0.944 -3.203,0.204c-1.139,-0.721 -1.505,-1.711 -1.397,-2.964c0.128,-1.487 0.271,-2.972 0.428,-4.683c-0.666,-0.273 -1.329,-0.554 -1.999,-0.816c-0.692,-0.271 -1.439,-0.442 -2.076,-0.806c-0.871,-0.497 -1.611,-2.212 -1.29,-3.033c0.402,-1.03 1.054,-1.877 2.306,-2.007c1.484,-0.154 2.961,-0.413 4.449,-0.496c1.31,-0.073 2.229,-0.56 2.663,-1.83c0.614,-1.798 1.3,-3.576 1.818,-5.402c0.548,-1.934 1.235,-2.706 2.722,-2.679c1.553,0.028 2.468,0.811 2.643,2.367c0.21,1.888 0.358,3.784 0.554,5.886z" fill="#57829B" id="svg_41"/>
<path d="m715.687,156.829881c0.347,1.587 -0.356,2.857 -1.195,4.107c-0.56,0.834 -1.247,1.27 -2.278,1.028c-0.946,-0.222 -1.79,-0.693 -1.891,-1.687c-0.104,-1.019 0.107,-2.071 0.18,-3.108c0.053,-0.063 0.104,-0.129 0.16,-0.19c2.162,-2.296 2.731,-2.313 5.024,-0.15z" fill="#648CA3" id="svg_57"/>
<path d="m706.431,153.888881c-1.644,0.036 -3.161,-1.439 -3.192,-3.104c-0.028,-1.499 1.198,-2.548 3.001,-2.567c1.618,-0.017 3.153,1.284 3.199,2.789c0.058,1.866 -1.115,2.593 -3.008,2.882z" fill="#6F92A7" id="svg_58"/>
<path d="m703.429,159.265881c1.516,3.137 0.082,5.365 -3.374,5.289c-1.177,-0.026 -1.923,-0.435 -1.971,-1.714c-0.028,-0.747 -0.074,-1.493 -0.112,-2.241c1.107,-1.124 2.482,-1.565 4.025,-1.576c0.175,0.01 0.35,0.019 0.525,0.029c0.302,0.072 0.604,0.142 0.907,0.213z" fill="#6F92A7" id="svg_61"/>
<path d="m702.521,159.053881c-0.175,-0.009 -0.35,-0.019 -0.525,-0.029c0.175,0.009 0.35,0.019 0.525,0.029z" fill="#FEDBD0" id="svg_63"/>
<path d="m223.597,273.687881c-0.512,-6.061 -1.633,-11.978 -4.77,-17.286c-1.465,-2.479 -3.282,-4.749 -4.939,-7.113c4.654,-0.328 9.281,-0.385 13.788,1.146c0.932,0.317 1.925,0.51 2.906,0.615c4.351,0.469 7.965,2.621 11.503,4.953c1.017,0.67 1.987,1.413 2.979,2.123c-0.421,0.461 -0.758,1.086 -1.279,1.356c-4.186,2.163 -7.941,4.946 -11.694,7.767c-2.3,1.728 -4.625,3.375 -6.56,5.537c-0.438,0.486 -1.279,0.611 -1.934,0.902z" fill="#B2CDDD" id="svg_64"/>
<path d="m335.508,275.129881c-2.846,4.805 -6.708,8.808 -10.273,13.047c-0.69,0.82 -1.222,1.771 -1.845,2.649c-1.402,1.974 -3.702,2.424 -5.65,1.118c-1.385,-0.929 -1.593,-4.288 -0.312,-5.917c0.87,-1.105 1.82,-2.147 2.752,-3.203c2.797,-3.169 5.596,-6.336 8.407,-9.493c0.802,-0.9 1.646,-1.763 2.471,-2.642c0.73,-0.128 1.455,-0.312 2.191,-0.373c1.829,-0.152 2.838,0.825 2.697,2.636c-0.056,0.733 -0.286,1.452 -0.438,2.178z" fill="#B4CFDE" id="svg_66"/>
<path d="m365.213,292.639881c-2.923,0.011 -4.061,-1.133 -3.662,-3.836c0.188,-1.269 0.596,-2.692 1.385,-3.648c2.256,-2.732 4.501,-5.508 7.436,-7.601c2.437,-1.738 4.98,-1.037 5.309,1.618c0.102,0.82 -0.158,1.866 -0.631,2.545c-2.263,3.244 -4.637,6.413 -7.017,9.575c-0.726,0.965 -1.689,1.614 -2.82,1.347z" fill="#B7D0DE" id="svg_68"/>
<path d="m345.5,268.101881c-2.654,0.057 -4.406,-1.231 -4.777,-3.208c-0.12,-0.641 0.009,-1.577 0.414,-2.029c2.596,-2.899 5.285,-5.716 7.986,-8.519c0.445,-0.463 1.105,-0.714 1.651,-1.085c1.131,-0.771 2.18,-0.518 3.214,0.206c1.045,0.731 1.135,1.807 0.959,2.898c-0.359,2.215 -1.79,3.89 -3.09,5.585c-1.255,1.636 -2.622,3.204 -4.082,4.659c-0.761,0.758 -1.828,1.209 -2.275,1.493z" fill="#B7D0DE" id="svg_69"/>
<path d="m362.953,268.123881c-2.726,-0.059 -4.533,-2.69 -3.744,-5.101c0.43,-1.314 1.096,-2.661 2.005,-3.683c1.368,-1.538 2.89,-3.059 4.636,-4.101c1.254,-0.749 3.001,-1.022 4.494,-0.932c1.821,0.11 2.8,2.638 1.747,4.331c-0.991,1.594 -1.889,3.258 -3.578,4.394c-1.045,0.703 -1.697,2 -2.499,3.05c-0.892,1.168 -1.963,2 -3.061,2.042z" fill="#B7D0DE" id="svg_70"/>
<path d="m346.953,290.630881c-0.389,0.126 -0.788,0.224 -1.164,0.382c-1.732,0.724 -3.623,0.453 -4.766,-0.701c-0.755,-0.762 -0.782,-3.373 0.021,-4.694c0.423,-0.696 0.978,-1.312 1.474,-1.965c0.473,-0.289 1.11,-0.468 1.391,-0.887c1.734,-2.582 4.36,-3.985 6.856,-5.494c2.881,1.421 3.464,2.328 2.74,4.45c-0.288,0.844 -0.712,1.688 -1.258,2.389c-1.717,2.21 -3.522,4.352 -5.294,6.52z" fill="#B7D0DE" id="svg_71"/>
<path d="m57.51,258.254881c-0.725,0.387 -1.413,0.888 -2.182,1.141c-5.545,1.826 -10.189,5.148 -14.687,8.726c-0.315,0.251 -0.713,0.398 -1.073,0.593c-0.061,-0.064 -0.122,-0.126 -0.184,-0.188c0.378,-0.802 0.647,-1.682 1.152,-2.394c2.752,-3.881 5.562,-7.72 8.352,-11.574c0.196,1.12 1.068,0.99 1.89,1.103c2.438,0.336 4.816,0.879 6.732,2.593z" fill="#B2CDDD" id="svg_72"/>
<path d="m270.464,237.841881c-0.074,1.702 -1.733,3.845 -3.338,4.195c-1.004,0.219 -1.98,0.172 -2.694,-0.786c-0.887,-1.19 -1.42,-2.497 -0.845,-3.92c0.328,-0.812 0.93,-1.582 1.586,-2.177c0.994,-0.901 2.15,-1.678 3.602,-0.953c1.113,0.558 1.759,2.025 1.689,3.641z" fill="#5E879F" id="svg_77"/>
<path d="m286.407,233.645881c-0.038,2.102 -1.875,3.799 -4.097,3.782c-1.9,-0.014 -3.265,-1.56 -3.191,-3.616c0.07,-1.971 2.163,-4.021 4.041,-3.957c1.579,0.054 3.278,2.038 3.247,3.791z" fill="#5D859D" id="svg_79"/>
<path d="m265.936,251.172881c0.663,1.477 -0.088,2.766 -0.685,3.985c-0.662,1.351 -3.405,1.914 -4.439,0.312c-0.486,-0.754 -0.896,-1.557 -1.34,-2.338c0.027,-0.161 0.077,-0.321 0.078,-0.482c0.005,-1.561 0.793,-2.748 2.171,-3.216c1.382,-0.47 2.73,0.014 3.716,1.205c0.155,0.188 0.332,0.357 0.499,0.534z" fill="#5E879F" id="svg_81"/>
<path d="m298.765,241.316881c1.714,-0.007 3.078,1.376 3.141,3.186c0.065,1.862 -1.575,3.436 -3.519,3.378c-1.716,-0.052 -3.134,-1.535 -3.102,-3.246c0.029,-1.599 1.824,-3.311 3.48,-3.318z" fill="#5E879F" id="svg_82"/>
<path d="m282.445,251.329881c0.463,0.342 0.949,0.658 1.385,1.031c1.197,1.024 1.613,2.333 1.157,3.529c-0.5,1.314 -2.117,2.391 -3.526,2.301c-1.144,-0.073 -2.149,-0.514 -2.592,-1.627c-0.515,-1.295 -0.507,-2.629 0.49,-3.73c0.49,-0.54 1.145,-0.931 1.725,-1.388c0.454,-0.038 0.908,-0.077 1.361,-0.116z" fill="#5E879F" id="svg_83"/>
<path d="m275.434,250.637881c-0.544,1.018 -0.621,2.398 -2.288,2.46c-1.627,0.06 -2.849,-0.425 -3.551,-1.953c-0.009,-0.574 -0.08,-1.155 -0.016,-1.721c0.219,-1.933 1.62,-3.182 3.424,-3.108c1.562,0.064 2.446,1.248 2.477,3.336c0.005,0.328 -0.03,0.657 -0.046,0.986z" fill="#648CA3" id="svg_85"/>
<path d="m291.468,251.583881c-1.29,0.487 -2.489,0.34 -3.583,-0.518c-0.97,-2.772 -0.614,-3.836 1.427,-5.153c1.211,-0.782 2.179,-0.419 3.161,0.28c1.001,0.713 1.325,1.786 0.845,2.838c-0.429,0.938 -1.219,1.709 -1.85,2.553z" fill="#648CA3" id="svg_86"/>
<path d="m281.534,241.295881c1.544,-0.012 2.727,1.275 2.695,2.933c-0.03,1.558 -1.463,2.836 -3.168,2.825c-1.485,-0.01 -2.853,-1.174 -2.926,-2.491c-0.089,-1.609 1.621,-3.253 3.399,-3.267z" fill="#648CA3" id="svg_87"/>
<path d="m293.736,231.754881c1.644,0.208 2.646,1.505 2.404,3.111c-0.277,1.838 -1.463,3.036 -2.797,2.827c-1.772,-0.278 -3.009,-1.846 -2.752,-3.487c0.254,-1.615 1.58,-2.649 3.145,-2.451z" fill="#648CA3" id="svg_88"/>
<path d="m593.498,223.123881c2.643,-1.967 5.188,-4.085 7.95,-5.869c4.826,-3.116 10.023,-5.563 15.496,-7.29c2.742,-0.865 5.661,-1.167 8.5,-1.723c-2.26,1.482 -4.49,3.011 -6.786,4.435c-5.762,3.575 -11.658,6.908 -18.194,8.877c-2.273,0.686 -4.641,1.056 -6.966,1.57z" fill="#FDFDFE" id="svg_122"/>
<path d="m676.375,263.677881c-1.33,-0.165 -2.321,-0.213 -3.273,-0.429c-0.848,-0.193 -1.521,-0.721 -1.505,-1.71c0.016,-1.015 0.557,-1.935 1.518,-1.987c2.362,-0.128 4.745,-0.09 7.104,0.089c0.519,0.039 1.342,0.936 1.355,1.454c0.014,0.556 -0.651,1.434 -1.215,1.641c-1.365,0.5 -2.844,0.691 -3.984,0.942z" fill="#EDD1CA" id="svg_131"/>
<path d="m707.815,258.044881c1.271,0.174 2.745,0.353 4.21,0.585c0.81,0.129 1.39,0.567 1.359,1.482c-0.03,0.897 -0.622,1.374 -1.424,1.445c-2.382,0.211 -4.77,0.363 -7.159,0.498c-1.207,0.068 -1.898,-0.673 -2.005,-1.764c-0.103,-1.041 0.632,-1.708 1.602,-1.897c1.047,-0.203 2.13,-0.226 3.417,-0.349z" fill="#EDD1CA" id="svg_132"/>
<path d="m726.254,240.603881c-1.297,-0.094 -2.132,-0.084 -2.94,-0.23c-1.079,-0.195 -1.634,-1.103 -1.446,-2.009c0.133,-0.642 0.94,-1.549 1.532,-1.621c2.211,-0.265 4.457,-0.303 6.689,-0.279c0.819,0.009 1.835,0.469 1.701,1.451c-0.092,0.674 -0.755,1.594 -1.369,1.807c-1.466,0.51 -3.055,0.666 -4.167,0.881z" fill="#EDD1CA" id="svg_133"/>
<path d="m698.203,251.614881c-0.58,0 -1.209,0.142 -1.727,-0.036c-0.633,-0.219 -1.599,-0.652 -1.655,-1.091c-0.087,-0.684 0.341,-1.55 0.802,-2.145c0.295,-0.381 1.033,-0.548 1.585,-0.577c1.567,-0.083 3.154,-0.164 4.705,0.005c0.566,0.062 1.292,0.731 1.526,1.297c0.365,0.882 -0.206,1.686 -1.088,1.923c-1.345,0.361 -2.742,0.525 -4.117,0.773c-0.01,-0.05 -0.02,-0.1 -0.031,-0.149z" fill="#EDD1CA" id="svg_134"/>
<path d="m715.076,280.980881c-1.135,0 -1.906,0.215 -2.424,-0.055c-0.648,-0.338 -1.446,-1.033 -1.518,-1.651c-0.072,-0.612 0.568,-1.78 1.087,-1.904c1.89,-0.452 3.887,-1.119 5.789,-0.143c0.643,0.33 1.382,1.06 1.491,1.706c0.174,1.034 -0.71,1.569 -1.714,1.707c-1.056,0.147 -2.116,0.266 -2.711,0.34z" fill="#EDD1CA" id="svg_135"/>
<path d="m652.969,279.278881c-1.25,-0.064 -2.178,-0.031 -3.074,-0.181c-0.87,-0.146 -1.738,-0.696 -1.508,-1.636c0.165,-0.674 0.957,-1.671 1.5,-1.692c2.221,-0.083 4.462,0.08 6.676,0.329c0.48,0.054 1.229,0.845 1.219,1.282c-0.011,0.471 -0.695,1.191 -1.214,1.333c-1.265,0.348 -2.607,0.425 -3.599,0.565z" fill="#EDD1CA" id="svg_136"/>
<path d="m709.683,290.088881c0.787,0.211 1.882,0.354 2.833,0.801c1.011,0.476 1.098,1.653 0.147,2.201c-1.596,0.919 -3.36,1.443 -5.244,1.002c-0.962,-0.225 -1.728,-0.773 -1.698,-1.861c0.029,-1.04 0.724,-1.662 1.699,-1.888c0.64,-0.148 1.313,-0.154 2.263,-0.255z" fill="#EDD1CA" id="svg_137"/>
<path d="m735.998,252.586881c-0.79,-0.092 -1.214,-0.085 -1.604,-0.198c-1.005,-0.291 -1.885,-0.81 -1.904,-1.985c-0.02,-1.232 0.879,-1.808 1.937,-1.817c1.452,-0.012 2.972,0.005 4.34,0.418c1.423,0.429 1.466,1.845 0.137,2.548c-0.995,0.527 -2.14,0.771 -2.906,1.034z" fill="#EDD1CA" id="svg_138"/>
<path d="m736.343,271.796881c-2.273,-0.125 -3.097,-0.611 -3.183,-1.997c-0.078,-1.265 1.025,-2.049 3.079,-2.046c0.969,0.001 1.987,0.067 2.888,0.376c0.505,0.173 1.16,0.925 1.134,1.38c-0.034,0.591 -0.556,1.402 -1.089,1.659c-0.846,0.409 -1.876,0.438 -2.829,0.628z" fill="#EDD1CA" id="svg_139"/>
<path d="m685.329,281.473881c-0.519,-0.091 -1.172,-0.177 -1.811,-0.324c-0.899,-0.207 -1.443,-0.763 -1.482,-1.72c-0.039,-0.96 0.485,-1.524 1.335,-1.857c1.168,-0.457 4.739,0.394 5.578,1.336c0.727,0.816 0.512,1.843 -0.548,2.125c-0.947,0.252 -1.951,0.289 -3.072,0.44z" fill="#EDD1CA" id="svg_140"/>
<path d="m740.005,293.233881c-1.484,2.038 -4.82,2.573 -6.338,1.091c-0.458,-0.447 -0.802,-1.318 -0.712,-1.921c0.068,-0.455 0.896,-1.103 1.414,-1.13c2.041,-0.107 4.097,-0.01 5.636,1.96z" fill="#EDD1CA" id="svg_141"/>
<path d="m676.726,294.574881c-1.711,-0.04 -3.084,-1.214 -3.063,-2.22c0.021,-0.962 1.182,-1.772 2.604,-1.715c0.987,0.039 2.016,0.066 2.934,0.373c0.515,0.172 1.134,0.88 1.173,1.388c0.036,0.475 -0.525,1.241 -1.011,1.458c-0.96,0.43 -2.05,0.569 -2.637,0.716z" fill="#EDD1CA" id="svg_142"/>
<path d="m645.368,290.254881c0.633,0.063 1.151,0.028 1.608,0.179c0.814,0.269 1.658,0.774 1.413,1.727c-0.148,0.577 -0.82,1.246 -1.4,1.436c-0.992,0.324 -2.103,0.442 -3.152,0.39c-0.94,-0.046 -1.703,-0.571 -1.744,-1.718c-0.038,-1.047 0.646,-1.474 1.465,-1.719c0.622,-0.188 1.291,-0.216 1.81,-0.295z" fill="#EDD1CA" id="svg_143"/>
<path d="m274.546,264.670881c-0.023,2.518 -1.747,4.264 -4.225,4.279c-1.497,0.009 -2.755,-1.173 -2.755,-2.588c0,-2.289 2.2,-4.961 4.097,-4.975c1.785,-0.014 2.901,1.257 2.883,3.284z" fill="#5C869F" id="svg_145"/>
<path d="m300.5,258.759881c-0.01,1.443 -1.51,2.701 -3.254,2.729c-2.272,0.036 -3.682,-1.048 -3.628,-2.788c0.068,-2.185 1.488,-3.76 3.444,-3.82c1.704,-0.053 3.451,1.919 3.438,3.879z" fill="#5C869F" id="svg_146"/>
<path d="m288.982,264.797881c-0.042,2.309 -0.991,3.358 -2.981,3.296c-1.448,-0.045 -2.113,-0.562 -2.702,-2.102c-0.46,-1.2 -0.029,-2.816 1.055,-3.791c0.917,-0.825 2,-1.185 3.198,-0.77c1.111,0.384 1.468,1.319 1.43,3.367z" fill="#5E879F" id="svg_147"/>
<path d="m54.996,243.147881c-0.296,0.503 -0.591,1.005 -1.117,1.898c-0.385,-1.33 0.001,-1.917 0.546,-2.46c0.191,0.188 0.381,0.375 0.571,0.562z" fill="#B7D0DE" id="svg_148"/>
<path d="m54.996,243.147881c-0.19,-0.187 -0.381,-0.375 -0.571,-0.562c0.19,0.188 0.381,0.375 0.571,0.562z" fill="#FFF6F3" id="svg_149"/>
<path d="m85.732,206.903881c-0.237,0.025 -0.475,0.049 -0.712,0.074c0.092,-0.177 0.185,-0.353 0.276,-0.531c0.145,0.152 0.291,0.304 0.436,0.457z" fill="#FFF6F3" id="svg_150"/>
<path d="m38.938,269.085881c0.15,-0.185 0.299,-0.37 0.447,-0.557c0.061,0.06 0.122,0.123 0.183,0.187c-0.174,0.159 -0.349,0.318 -0.523,0.477l-0.107,-0.107z" fill="#B2CDDD" id="svg_151"/>
<path d="m39.045,269.192881c-0.085,0.101 -0.169,0.202 -0.254,0.303c0.049,-0.136 0.099,-0.272 0.148,-0.409c-0.001,-0.001 0.106,0.106 0.106,0.106z" fill="#B2CDDD" id="svg_152"/>
<path d="m152.512,257.161881c3.724,-3.191 7.427,-6.406 11.178,-9.566c5.138,-4.329 10.983,-7.111 17.688,-8.031c4.144,-0.569 8.325,-1.086 12.392,0.074c3.417,0.975 6.74,2.334 10.019,3.724c2.576,1.092 4.493,3.109 6.227,5.273c-0.363,0.164 -0.719,0.451 -1.09,0.473c-3.391,0.201 -6.791,0.268 -10.175,0.546c-4.61,0.379 -9.215,0.848 -13.808,1.394c-3.524,0.419 -7.03,0.992 -10.537,1.545c-3.095,0.488 -6.184,1.022 -9.267,1.587c-0.554,0.102 -1.122,0.37 -1.581,0.702c-1.272,0.919 -2.694,1.102 -4.212,1.261c-1.542,0.161 -3.047,0.703 -4.557,1.123c-0.786,0.218 -1.553,0.736 -2.277,-0.105z" fill="#FCF4F2" id="svg_153"/>
<path d="m511.621,227.118881c-3.305,-4.685 -4.944,-9.95 -5.001,-15.629c-0.044,-4.401 1.644,-8.317 4.647,-11.502c4.067,-4.313 8.555,-3.465 12.441,-0.361c3.542,2.83 5.056,6.77 5.474,11.091c0.534,5.52 -0.699,10.843 -2.418,16.059c-0.983,-0.041 -1.967,-0.142 -2.947,-0.111c-4.066,0.131 -8.131,0.299 -12.196,0.453z" fill="#FDFDFE" id="svg_154"/>
<path d="m651.961,221.104881c0.457,1.649 1.046,3.198 1.302,4.8c1.302,8.173 -0.427,15.373 -7.132,20.786c-1.133,0.914 -2.531,1.579 -3.906,2.097c-2.092,0.788 -3.43,0.253 -4.71,-1.598c-1.317,-1.906 -1.929,-4.125 -1.956,-6.347c-0.043,-3.592 0.49,-7.119 2.83,-10.114c3.502,-4.483 7.622,-8.064 13.572,-9.624z" fill="#FCDACF" id="svg_155"/>
<path d="m511.621,227.118881c4.065,-0.154 8.13,-0.323 12.197,-0.451c0.98,-0.031 1.964,0.07 2.947,0.111c-1.078,3.612 -2.945,6.8 -5.242,10.06c-4.142,-2.56 -7.428,-5.713 -9.902,-9.72z" fill="#FCDACF" id="svg_156"/>
<path d="m137.585,256.566881c-0.232,1.81 -0.466,3.621 -0.694,5.431c-0.152,1.21 -0.755,1.98 -2.018,2.163c-2.277,0.33 -4.455,1.03 -6.873,0.68c-4.262,-0.616 -6.739,-3.083 -7.888,-6.96c-1.494,-5.037 -0.23,-9.754 2.481,-14.042c2.013,-3.184 5.467,-3.625 8.715,-1.513c3.204,2.084 4.885,5.144 5.587,8.784c0.345,1.788 0.465,3.616 0.69,5.457z" fill="#FCF4F2" id="svg_163"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

+7
View File
@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="256px" height="256px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path opacity="0.5" d="M3 10C3 6.22876 3 4.34315 4.17157 3.17157C5.34315 2 7.22876 2 11 2H13C16.7712 2 18.6569 2 19.8284 3.17157C21 4.34315 21 6.22876 21 10V14C21 17.7712 21 19.6569 19.8284 20.8284C18.6569 22 16.7712 22 13 22H11C7.22876 22 5.34315 22 4.17157 20.8284C3 19.6569 3 17.7712 3 14V10Z" fill="#c67e48"/> <path d="M16.5189 16.5013C16.6939 16.3648 16.8526 16.2061 17.1701 15.8886L21.1275 11.9312C21.2231 11.8356 21.1793 11.6708 21.0515 11.6264C20.5844 11.4644 19.9767 11.1601 19.4083 10.5917C18.8399 10.0233 18.5356 9.41561 18.3736 8.94849C18.3292 8.82066 18.1644 8.77687 18.0688 8.87254L14.1114 12.8299C13.7939 13.1474 13.6352 13.3061 13.4987 13.4811C13.3377 13.6876 13.1996 13.9109 13.087 14.1473C12.9915 14.3476 12.9205 14.5606 12.7786 14.9865L12.5951 15.5368L12.3034 16.4118L12.0299 17.2323C11.9601 17.4419 12.0146 17.6729 12.1708 17.8292C12.3271 17.9854 12.5581 18.0399 12.7677 17.9701L13.5882 17.6966L14.4632 17.4049L15.0135 17.2214L15.0136 17.2214C15.4394 17.0795 15.6524 17.0085 15.8527 16.913C16.0891 16.8004 16.3124 16.6623 16.5189 16.5013Z" fill="#c67e48"/> <path d="M22.3665 10.6922C23.2112 9.84754 23.2112 8.47812 22.3665 7.63348C21.5219 6.78884 20.1525 6.78884 19.3078 7.63348L19.1806 7.76071C19.0578 7.88348 19.0022 8.05496 19.0329 8.22586C19.0522 8.33336 19.0879 8.49053 19.153 8.67807C19.2831 9.05314 19.5288 9.54549 19.9917 10.0083C20.4545 10.4712 20.9469 10.7169 21.3219 10.847C21.5095 10.9121 21.6666 10.9478 21.7741 10.9671C21.945 10.9978 22.1165 10.9422 22.2393 10.8194L22.3665 10.6922Z" fill="#c67e48"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.25 9C7.25 8.58579 7.58579 8.25 8 8.25H14.5C14.9142 8.25 15.25 8.58579 15.25 9C15.25 9.41421 14.9142 9.75 14.5 9.75H8C7.58579 9.75 7.25 9.41421 7.25 9ZM7.25 13C7.25 12.5858 7.58579 12.25 8 12.25H11C11.4142 12.25 11.75 12.5858 11.75 13C11.75 13.4142 11.4142 13.75 11 13.75H8C7.58579 13.75 7.25 13.4142 7.25 13ZM7.25 17C7.25 16.5858 7.58579 16.25 8 16.25H9.5C9.91421 16.25 10.25 16.5858 10.25 17C10.25 17.4142 9.91421 17.75 9.5 17.75H8C7.58579 17.75 7.25 17.4142 7.25 17Z" fill="#c67e48"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+406
View File
@@ -0,0 +1,406 @@
<?xml version="1.0"?>
<svg width="180" height="140" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<g id="BACKGROUND"/>
<g id="OBJECTS">
<g id="svg_2">
<g id="svg_3">
<g id="svg_5"/>
<g id="svg_20"/>
<g id="svg_35"/>
</g>
<g id="svg_48">
<g id="svg_51"/>
<g id="svg_56"/>
<g id="svg_70">
<g id="svg_74"/>
<g id="svg_76"/>
<g id="svg_78"/>
<g id="svg_80"/>
</g>
<g id="svg_82">
<g id="svg_86"/>
<g id="svg_88"/>
<g id="svg_90"/>
<g id="svg_92"/>
</g>
<g id="svg_95"/>
<g id="svg_100"/>
</g>
<g id="svg_104">
<g id="svg_116"/>
<g id="svg_118"/>
<g id="svg_126"/>
<g id="svg_135"/>
</g>
<g id="svg_146">
<g id="svg_155"/>
<g id="svg_157"/>
<g id="svg_165"/>
<g id="svg_175"/>
<g id="svg_180">
<g id="svg_184"/>
<g id="svg_186"/>
<g id="svg_188"/>
<g id="svg_190"/>
</g>
<g id="svg_192">
<g id="svg_196"/>
<g id="svg_198"/>
<g id="svg_200"/>
<g id="svg_202"/>
</g>
</g>
<g id="svg_205">
<g id="svg_206"/>
<g id="svg_209"/>
</g>
<g id="svg_213">
<g id="svg_214">
<g id="svg_216"/>
</g>
<g id="svg_219">
<g id="svg_221"/>
</g>
<g id="svg_224">
<g id="svg_226"/>
</g>
<g id="svg_228"/>
</g>
</g>
<g id="svg_231">
<g id="svg_232">
<g id="svg_233">
<g id="svg_234">
<path d="m76.933,96.939l-6.102,30.954l-0.712,0l4.014,-32.367c0,0 2.8,2.119 2.8,1.413z" fill="#2D294C" id="svg_235"/>
<path d="m81.83,96.939l-6.102,30.954l-0.712,0l4.014,-32.367c0.001,0 2.8,2.119 2.8,1.413z" fill="#2D294C" id="svg_236"/>
</g>
<g id="svg_237">
<path d="m98.866,96.939l6.102,30.954l0.712,0l-4.014,-32.367c-0.001,0 -2.8,2.119 -2.8,1.413z" fill="#2D294C" id="svg_238"/>
<path d="m93.968,96.939l6.102,30.954l0.712,0l-4.014,-32.367c0,0 -2.8,2.119 -2.8,1.413z" fill="#2D294C" id="svg_239"/>
</g>
</g>
<path d="m103.771,77.118l-3.711,-11.503c-1.121,-3.475 -4.355,-5.83 -8.007,-5.83l-1.946,0l-1.864,0l-4.498,0c-3.651,0 -6.886,2.355 -8.007,5.83l-3.711,11.503l-0.748,0c-1.852,0 -3.353,1.501 -3.353,3.353l0,5.309c0,6.848 5.551,12.399 12.399,12.399l7.917,0l1.864,0l5.365,0c6.848,0 12.399,-5.551 12.399,-12.399l0,-5.309c0,-1.852 -1.501,-3.353 -3.353,-3.353l-0.746,0z" fill="#9292AA" id="svg_240"/>
<g id="svg_241">
<g id="svg_242">
<rect fill="#2D294C" height="30.178" id="svg_243" transform="matrix(0.374 -0.9274 0.9274 0.374 31.4581 419.478)" width="0.486" x="310.463764" y="-78.932512"/>
</g>
<g id="svg_244">
<rect fill="#2D294C" height="0.486" id="svg_245" transform="matrix(0.9271 -0.3748 0.3748 0.9271 -46.1495 135.573)" width="30.178" x="119.351398" y="23.748997"/>
</g>
</g>
<path d="m103.942,77.118l-0.171,0l-3.711,-11.503c-1.121,-3.475 -4.355,-5.83 -8.007,-5.83l-1.946,0l-1.864,0l-4.498,0c-3.651,0 -6.886,2.355 -8.007,5.83l-3.711,11.503l-0.01,0l0,12.579c0,4.58 3.633,8.447 8.213,8.482c0.032,0 0.064,0 0.096,0l7.917,0l1.864,0l5.365,0c0.088,0 0.176,-0.001 0.264,-0.003c4.577,-0.096 8.206,-3.907 8.206,-8.485l0,-12.573z" fill="#CFCCDF" id="svg_246"/>
<path d="m70.573,93.419c2.27,2.894 5.79,4.76 9.753,4.76l7.917,0l1.864,0l5.365,0c3.963,0 7.483,-1.866 9.753,-4.76l-34.652,0z" fill="#9292AA" id="svg_247"/>
</g>
<g id="svg_248">
<g id="svg_249"/>
<g id="svg_251"/>
<g id="svg_253"/>
<g id="svg_255"/>
<g id="svg_260"/>
<g id="svg_264"/>
<g id="svg_268">
<g id="svg_269"/>
<g id="svg_271"/>
<g id="svg_273"/>
<g id="svg_275"/>
<g id="svg_277"/>
<g id="svg_279"/>
<g id="svg_281"/>
<g id="svg_283"/>
<g id="svg_285"/>
<g id="svg_287"/>
<g id="svg_289"/>
<g id="svg_291"/>
<g id="svg_293"/>
<g id="svg_295"/>
<g id="svg_297"/>
</g>
</g>
<g id="svg_299">
<g id="svg_300"/>
<g id="svg_308"/>
<g id="svg_313"/>
<g id="svg_315">
<g id="svg_319"/>
<g id="svg_321"/>
<g id="svg_323"/>
<g id="svg_325"/>
</g>
<g id="svg_329"/>
</g>
<g id="svg_345">
<polygon fill="#D7914E" id="svg_346" points="105.989990234375,118.74800109863281 106.02999877929688,125.25300598144531 106.30999755859375,127.28900146484375 109.94100952148438,127.48800659179688 110.94900512695312,126.00199890136719 110.30099487304688,117.8699951171875 "/>
<rect fill="#2D294C" height="15.55" id="svg_347" width="21.717" x="76.059" y="78.354"/>
<path d="m68.326,77.424c-0.902,-0.263 -1.782,-0.6 -2.629,-1.005c-1.383,-0.661 -2.848,-1.829 -2.702,-3.355c0.096,-1.006 0.995,-3.146 1.705,-3.865c1.915,-1.94 7.913,-13.124 9.945,-14.721c0.888,-0.698 9.498,-4.708 9.904,-2.435c0.077,0.43 -0.507,3.353 -0.629,3.772c-0.972,3.357 -3.12,4.862 -5.219,7.656c-1.819,2.422 -4.013,4.552 -5.728,7.048c0.632,1.036 1.411,2.12 2.385,2.843c3.033,2.25 6.071,4.491 9.116,6.724c-0.729,0.863 -1.156,1.903 -1.474,2.987c-5.004,-1.626 -9.621,-4.173 -14.674,-5.649z" fill="#D7914E" id="svg_348"/>
<path d="m103.921,77.424c0.902,-0.263 1.782,-0.6 2.629,-1.005c1.383,-0.661 2.848,-1.829 2.702,-3.355c-0.096,-1.006 -0.995,-3.146 -1.705,-3.865c-1.915,-1.94 -7.913,-13.124 -9.945,-14.721c-0.888,-0.698 -9.498,-4.708 -9.904,-2.435c-0.077,0.43 0.507,3.353 0.629,3.772c0.972,3.357 3.12,4.862 5.219,7.656c1.819,2.422 4.013,4.552 5.728,7.048c-0.632,1.036 -1.411,2.12 -2.385,2.843c-3.033,2.25 -6.071,4.491 -9.116,6.724c0.729,0.863 1.156,1.903 1.474,2.987c5.004,-1.626 9.621,-4.173 14.674,-5.649z" fill="#D7914E" id="svg_349"/>
<path d="m92.764,74.378c0,1.592 -1.348,2.882 -3.011,2.882c-1.663,0 -3.011,-1.29 -3.011,-2.882c0,-1.592 1.348,-2.882 3.011,-2.882c1.663,-0.001 3.011,1.29 3.011,2.882z" fill="#A0616B" id="svg_350"/>
<g id="svg_351">
<path d="m104.514,121.741l-0.079,-1.984c-0.668,-10.878 -2.243,-21.139 -3.588,-24.237c-1.649,-1.06 -7.638,-2.66 -13.028,-3.43l-4.808,-0.687l1.373,-9.615l4.808,0.687c4.352,0.622 14.889,2.484 18.744,6.338c1.096,1.095 4.427,1.511 6.193,30.349l-0.116,2.139l-9.499,0.44zm-3.804,-26.502l0,0l0,0z" fill="#2D294C" id="svg_352"/>
</g>
<g id="svg_353">
<path d="m75.533,124.821l-0.868,-4.778c-0.162,-0.892 -2.938,-16.203 -3.432,-23.825c-0.527,-8.127 4.433,-11.396 7.13,-11.99l4.743,-1.044l2.088,9.485l-4.083,0.899c-0.103,0.258 -0.261,0.86 -0.186,2.021c0.458,7.067 3.258,22.503 3.296,22.716l0.869,4.778l-9.557,1.738z" fill="#2D294C" id="svg_354"/>
</g>
<path d="m99.534,58.12c-0.233,-3.776 -5.585,-7.346 -12.204,-7.346c-6.619,0 -12.365,1.538 -13.534,5.16c-0.585,1.811 2.424,21.23 2.401,22.425c-0.075,3.845 -0.257,5.671 -0.257,5.671l21.993,0.187c0,0 0.083,-2.617 -0.09,-5.498c-0.11,-1.83 1.812,-18.636 1.691,-20.599z" fill="#D7914E" id="svg_355"/>
<path d="m86.871,53.229l-0.575,0c-1.537,0 -2.783,-1.246 -2.783,-2.783l0,-8.716l6.141,0l0,8.716c0,1.537 -1.246,2.783 -2.783,2.783z" fill="#EFAFC0" id="svg_356"/>
<path d="m83.514,44.073c0.813,2.108 2.586,5.356 6.14,6.155l0,-8.499l-5.55,0l-0.59,0.259l0,2.085z" fill="#D98E9D" id="svg_357" opacity="0.5"/>
<path d="m88.507,31.913l-1.4,0.188c-2.784,0.374 -4.738,2.933 -4.364,5.717l0.64,4.77c0.378,2.815 2.966,4.791 5.781,4.413l2.485,-0.334c2.154,-0.289 3.665,-2.269 3.376,-4.423l-0.801,-5.967c-0.373,-2.783 -2.933,-4.737 -5.717,-4.364z" fill="#EFAFC0" id="svg_358"/>
<path d="m85.319,41.204c0.201,1.498 -0.85,2.874 -2.348,3.075c-1.497,0.201 -2.874,-0.85 -3.075,-2.348c-0.201,-1.498 0.85,-2.874 2.348,-3.075c1.497,-0.201 2.874,0.85 3.075,2.348z" fill="#EFAFC0" id="svg_359"/>
<path d="m82.98,39.098c0.342,0.948 0.611,1.922 0.805,2.91c0.098,-0.121 0.301,-0.197 0.452,-0.161" fill="none" id="svg_360"/>
<path d="m79.405,36.266c0.064,0.203 0.171,0.389 0.277,0.573c0.448,0.779 0.895,1.558 1.343,2.337c0.533,-0.343 1.228,-0.251 1.862,-0.25c0.168,0.696 0.253,1.418 0.279,2.134c0.23,-0.047 0.468,-0.064 0.703,-0.052c-0.081,-0.824 -0.109,-1.66 -0.135,-2.487c0.207,-0.25 0.524,-0.439 0.753,-0.669c-0.368,-0.4 -0.571,-0.938 -0.615,-1.479c-0.056,-0.687 0.26,-1.236 0.6,-1.836c0.34,-0.6 0.843,-1.132 1.487,-1.379c1.383,-0.531 2.731,-1.006 4.207,-0.891c0.582,0.045 1.221,0.088 1.716,0.399c0.765,-0.017 1.343,-0.848 1.326,-1.613c-0.017,-0.765 -0.473,-1.453 -1.003,-2.004c-0.374,-0.389 -0.797,-0.736 -1.275,-0.987c-0.595,-0.313 -1.261,-0.47 -1.928,-0.554c-1.757,-0.221 -3.575,0.058 -5.185,0.796c0.294,0.06 0.505,0.162 0.799,0.222c-0.498,0.05 -0.985,0.172 -1.483,0.222c-0.53,0.053 -1.065,0.108 -1.57,0.277c-0.922,0.309 -1.687,0.985 -2.259,1.772c-0.572,0.787 -0.969,1.684 -1.345,2.581c-0.163,0.389 -0.326,0.803 -0.264,1.22c0.051,0.347 0.254,0.652 0.469,0.93c0.272,0.349 0.797,0.732 1.241,0.738z" fill="#2D294C" id="svg_361"/>
<path d="m90.769,42.967l-2.971,-0.714c0,0 0.493,1.603 1.441,1.755c0.947,0.15 1.53,-1.041 1.53,-1.041z" fill="#FFFFFF" id="svg_362"/>
<path d="m89.656,38.335c0.04,0.302 -0.171,0.58 -0.473,0.62c-0.302,0.04 -0.58,-0.171 -0.62,-0.473c-0.04,-0.302 0.171,-0.58 0.473,-0.62c0.302,-0.041 0.579,0.171 0.62,0.473z" fill="#2D294C" id="svg_363"/>
<path d="m93.483,37.821c0.04,0.302 -0.171,0.58 -0.473,0.62c-0.302,0.041 -0.579,-0.171 -0.62,-0.473c-0.04,-0.302 0.171,-0.58 0.473,-0.62c0.301,-0.041 0.579,0.171 0.62,0.473z" fill="#2D294C" id="svg_364"/>
<polygon fill="#FFFFFF" id="svg_365" points="86.84201049804688,52.28199768066406 82.7130126953125,51.47599792480469 83.10000610351562,54.055999755859375 87.48599243164062,60.11799621582031 91.87100219726562,54.82899475097656 89.93600463867188,51.08900451660156 "/>
<path d="m83.382,49.978c0,0 3.161,1.747 6.287,0l0.359,1.527l-0.773,3.712l-2.315,-2.559l-1.996,2.689l-2.102,-3.483l0.54,-1.886z" fill="#EFF2F7" id="svg_366"/>
<g id="svg_367">
<path d="m77.129,132.034c0,0 -0.359,1.321 -0.166,2.048c0.193,0.728 0.945,2.038 2.77,2.094c1.825,0.057 4.203,0.173 4.061,-4.926l-6.665,0.784z" fill="#3F3E66" id="svg_368"/>
<path d="m79.816,123.896c0.162,-0.288 -0.354,-2.445 1.152,-2.371c1.506,0.074 1.678,0.793 1.567,1.853c-0.111,1.06 -0.807,5.388 -0.807,5.388l-2.112,0.193l0.2,-5.063z" fill="#2D294C" id="svg_369"/>
<path d="m79.5,122.394c0,0 -0.642,3.727 -0.709,3.871c-0.067,0.145 -1.992,5.885 -1.714,6.789c0.2,0.651 1.325,1.583 3.732,1.492c1.852,-0.07 3.219,-1.763 2.945,-3.596c-0.243,-1.626 -0.549,-4.7 -0.547,-4.746c0.005,-0.089 -0.298,-3.866 -0.298,-3.866s-1.764,2.646 -1.904,5.904l-0.65,-0.035c-0.001,0.001 0.162,-5.996 -0.855,-5.813z" fill="#2D294C" id="svg_370"/>
<g id="svg_371">
<path d="m82.319,124.27c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.056 -2.13,-0.157 -2.176,-0.118c-0.101,0.085 -0.252,0.073 -0.339,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.506,-0.432 1.748,-0.955 2.831,0.148c0.093,0.095 0.092,0.247 -0.003,0.34c-0.046,0.045 -0.107,0.068 -0.168,0.068z" fill="#3F3E66" id="svg_372"/>
</g>
<g id="svg_373">
<path d="m82.161,125.502c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.086 -0.252,0.073 -0.339,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.506,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.092,0.247 -0.003,0.34c-0.046,0.046 -0.107,0.069 -0.168,0.069z" fill="#3F3E66" id="svg_374"/>
</g>
<g id="svg_375">
<path d="m82.003,126.735c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.037,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.085 -0.252,0.073 -0.339,-0.028c-0.086,-0.102 -0.074,-0.252 0.027,-0.338c0.505,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.091,0.247 -0.003,0.34c-0.046,0.046 -0.107,0.069 -0.168,0.069z" fill="#3F3E66" id="svg_376"/>
</g>
<g id="svg_377">
<path d="m81.845,127.968c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.086 -0.252,0.073 -0.338,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.505,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.092,0.247 -0.003,0.34c-0.048,0.046 -0.109,0.069 -0.169,0.069z" fill="#3F3E66" id="svg_378"/>
</g>
</g>
<g id="svg_379">
<path d="m109.648,124.917c0,0 -0.906,-1.496 -0.021,-1.749c0.885,-0.253 1.434,0.241 1.434,0.241l2.491,2.198l1.365,1.652l-1.831,-0.581l-3.438,-1.761z" fill="#2D294C" id="svg_380"/>
<path d="m106.055,124.579c0,0 0.944,1.669 1.662,1.261c0.406,-0.231 0.985,-1.755 1.573,-1.617c0.588,0.138 2.961,1.476 3.837,1.794c0.876,0.318 -2.173,-2.574 -2.173,-2.574s2.521,1.913 3.304,2.2c0.784,0.286 7.197,1.07 7.591,2.986l0.064,1.261c0,0 -1.789,2.128 -6.783,1.15c-4.995,-0.978 -7.744,-0.907 -8.65,-1.133c-0.907,-0.227 -1.107,-5.191 -0.425,-5.328z" fill="#2D294C" id="svg_381"/>
<path d="m116.03,130.259c-2.331,-0.074 -6.343,-1.231 -7.828,-1.202c-0.855,0.017 -1.737,-0.166 -2.331,-0.325c0.14,0.637 0.344,1.108 0.609,1.174c0.907,0.226 3.656,0.155 8.65,1.133c4.995,0.978 6.783,-1.15 6.783,-1.15l-0.064,-1.261c-0.423,1.066 -3.488,1.705 -5.819,1.631z" fill="#3F3E66" id="svg_382"/>
<g id="svg_383">
<path d="m111.068,126.016c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.102 0.611,-0.059 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.176,-0.107 -0.34,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.877,1.073c-0.032,0.086 -0.108,0.142 -0.192,0.153z" fill="#3F3E66" id="svg_384"/>
</g>
<g id="svg_385">
<path d="m111.893,126.603c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.296,-0.102 0.611,-0.059 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.176,-0.107 -0.34,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.878,1.073c-0.032,0.086 -0.107,0.142 -0.191,0.153z" fill="#3F3E66" id="svg_386"/>
</g>
<g id="svg_387">
<path d="m112.861,126.952c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.1 0.612,-0.058 0.908,0.124c0.113,0.069 0.149,0.217 0.079,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.175,-0.108 -0.339,-0.133 -0.502,-0.078c-0.435,0.147 -0.781,0.824 -0.878,1.073c-0.032,0.085 -0.107,0.141 -0.191,0.152z" fill="#3F3E66" id="svg_388"/>
</g>
<g id="svg_389">
<path d="m113.865,127.252c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.103 0.611,-0.058 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.175,-0.107 -0.339,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.877,1.073c-0.033,0.086 -0.108,0.141 -0.192,0.153z" fill="#3F3E66" id="svg_390"/>
</g>
</g>
<path d="m85.292,83.099c-0.866,-0.069 -1.783,-0.265 -2.389,-0.888c-0.606,-0.622 0.539,-1.594 1.239,-2.108c0.968,0.44 2.076,-0.063 3.133,-0.182c0.641,-0.072 1.289,0.003 1.926,0.105c0.817,0.131 1.636,0.311 2.374,0.685c0.738,0.374 1.394,0.96 1.713,1.723c0.075,0.179 -0.024,0.35 -0.217,0.353c-0.194,0.003 -0.358,-0.133 -0.51,-0.253c-0.477,-0.376 -1.026,-0.66 -1.608,-0.834c-0.105,-0.031 -0.185,0.144 -0.086,0.191c1.456,0.689 1.801,1.611 1.775,1.694c-0.028,0.09 -0.139,0.124 -0.234,0.122c-0.171,-0.003 -0.452,-0.259 -0.594,-0.354c-0.603,-0.399 -1.146,-0.472 -1.146,-0.472s-0.619,0.036 0.899,0.801c0.187,0.094 0.381,0.221 0.443,0.421c0.011,0.035 0.017,0.073 0.01,0.109c-0.008,0.043 -0.035,0.081 -0.067,0.112c-0.098,0.093 -0.242,0.124 -0.376,0.112c-0.134,-0.011 -0.262,-0.06 -0.388,-0.109c-0.618,-0.237 -1.235,-0.475 -1.853,-0.712c0.163,0.103 1.282,0.662 1.403,0.813c0.047,0.059 0.092,0.131 0.077,0.204c-0.023,0.109 -0.157,0.146 -0.268,0.157c-0.743,0.074 -2.45,-0.542 -3.146,-0.813c-0.695,-0.269 -1.365,-0.818 -2.11,-0.877z" fill="#EFAFC0" id="svg_391"/>
<g id="svg_392">
<path d="m87.677,40.908c-1.249,0 -2.339,-0.926 -2.51,-2.197c-0.09,-0.671 0.086,-1.337 0.497,-1.875c0.411,-0.538 1.006,-0.884 1.677,-0.974c1.386,-0.186 2.663,0.79 2.849,2.174c0.186,1.385 -0.79,2.662 -2.175,2.849c-0.113,0.016 -0.226,0.023 -0.338,0.023zm0.003,-4.826c-0.102,0 -0.203,0.007 -0.306,0.021c-0.606,0.081 -1.145,0.394 -1.516,0.88c-0.372,0.487 -0.531,1.089 -0.45,1.695c0.169,1.252 1.324,2.137 2.575,1.966c1.252,-0.168 2.134,-1.324 1.966,-2.576c-0.154,-1.148 -1.139,-1.986 -2.269,-1.986z" fill="#2D294C" id="svg_393"/>
</g>
<g id="svg_394">
<path d="m93.067,40.184c-0.552,0 -1.086,-0.179 -1.532,-0.52c-0.538,-0.411 -0.884,-1.006 -0.974,-1.677c-0.186,-1.385 0.789,-2.663 2.174,-2.849c1.384,-0.184 2.663,0.79 2.849,2.175c0.09,0.671 -0.086,1.336 -0.497,1.874c-0.411,0.538 -1.006,0.884 -1.678,0.974c-0.114,0.015 -0.228,0.023 -0.342,0.023zm0.007,-4.826c-0.102,0 -0.203,0.007 -0.306,0.021c-1.252,0.168 -2.133,1.324 -1.965,2.576c0.082,0.607 0.394,1.145 0.88,1.517c0.488,0.372 1.094,0.532 1.695,0.449c0.607,-0.081 1.146,-0.394 1.517,-0.88c0.372,-0.487 0.531,-1.088 0.45,-1.695c-0.156,-1.15 -1.141,-1.988 -2.271,-1.988z" fill="#2D294C" id="svg_395"/>
</g>
<g id="svg_396">
<rect fill="#2D294C" height="0.242" id="svg_397" transform="matrix(0.9187 -0.3949 0.3949 0.9187 -20.6624 136.864)" width="2.505" x="133.483205" y="-48.057873"/>
</g>
<g id="svg_398">
<path d="m90.262,87.938l20.542,0l0,-1.506l-20.542,0c-0.416,0 -0.753,0.337 -0.753,0.753l0,0c0,0.415 0.337,0.753 0.753,0.753z" fill="#A8A5C4" id="svg_399"/>
<path d="m92.011,87.938l19.809,0c0.876,0 1.643,-0.59 1.867,-1.436l3.496,-13.172c0.325,-1.226 -0.599,-2.428 -1.867,-2.428l-17.297,0c-0.876,0 -1.643,0.589 -1.867,1.436l-4.141,15.6z" fill="#CFCCDF" id="svg_400"/>
<path d="m115.315,70.902c1.268,0 2.193,1.202 1.867,2.428l-3.496,13.172c-0.108,0.406 -0.342,0.751 -0.652,1.002l-12.041,-16.601l14.322,0l0,-0.001z" fill="#CFCCDF" id="svg_401"/>
<path d="m103.676,80.586c-0.217,0.859 0.324,1.556 1.208,1.556c0.884,0 1.776,-0.696 1.993,-1.556c0.217,-0.859 -0.324,-1.556 -1.208,-1.556c-0.884,0 -1.776,0.697 -1.993,1.556z" fill="#FFFFFF" id="svg_402"/>
</g>
</g>
<g id="svg_403">
<path d="m126.823,42.216l-2.049,-3.268c0,0 0.64,-1.378 -0.06,-2.56c-0.699,-1.182 -1.769,-3.318 -2.053,-3.046s0.287,2.342 0.287,2.342s-1.883,-3.209 -3.159,-3.401c-1.276,-0.193 1.325,3.309 1.325,3.309s-2.943,-3.063 -3.122,-2.455s1.769,3.318 1.933,3.488c0.164,0.17 -2.237,-2.215 -2.411,-1.829c-0.175,0.386 2.694,4.394 3.295,4.906c0.602,0.513 5.102,3.607 5.102,3.607l0.912,-1.093z" fill="#EFAFC0" id="svg_404"/>
<path d="m152.166,57.969l-0.035,-0.084l-9.44,-2.281l-1.141,4.72c-0.16,0.665 -4.486,17.513 -4.967,25.197c-0.286,4.586 11.326,34.167 12.018,35.98l0.099,1.43l-1.833,2.344c0,0 1.511,0.448 3.066,0.122c1.555,-0.326 2.9,-1.025 2.9,-1.025l-1.123,-4.079c0,0 -3.448,-21.082 -4.531,-27.574c-0.438,-2.627 -1.976,-4.794 -1.954,-5.541c0.256,-4.092 3.397,-11.984 5.887,-17.698c-0.193,5.235 -0.476,12.31 -0.735,16.441c-0.282,4.514 4.782,36.055 4.784,37.293l0.006,-0.012l-0.008,0.041l-2.354,2.58c0,0 1.511,0.448 3.066,0.122c1.555,-0.326 2.573,-1.911 2.573,-1.911s1.925,-36.981 2.142,-40.473c0.463,-7.413 0.469,-20.196 0.469,-20.737l0,-4.856l-8.889,0l0,0.001z" fill="#EFAFC0" id="svg_405"/>
<path d="m141.222,34.996c-0.081,1.39 0.046,2.794 -0.178,4.168c-0.224,1.374 -0.881,2.774 -2.104,3.438c-1.778,0.965 -3.985,0.055 -5.662,-1.077c-1.677,-1.132 -3.352,-2.546 -5.374,-2.628c-2.174,-0.088 -4.267,1.672 -4.554,3.829c-0.278,2.09 1.001,4.141 2.706,5.38c1.706,1.239 3.786,1.824 5.818,2.387c2.631,0.728 5.511,1.443 8.015,0.354c1.623,-0.706 2.845,-2.08 3.982,-3.437c2.616,-3.121 5.148,-6.53 5.932,-10.527c0.536,-2.731 0.007,-8.52 -3.222,-9.671c-3.815,-1.359 -5.207,5.17 -5.359,7.784z" fill="#5387C4" id="svg_406"/>
<path d="m163.17,56.037l0.452,10.385l-2.032,17.61l-25.06,-1.355l0.249,-2.57c0.734,-7.587 2.437,-15.048 5.067,-22.202l0.846,-2.301l20.478,0.433z" fill="#DC614B" id="svg_407"/>
<path d="m166.608,27.766c-1.695,-1.277 -2.558,-3.404 -2.871,-5.503c-0.313,-2.1 -0.152,-4.237 -0.217,-6.359c-0.046,-1.511 -0.218,-3.057 -0.898,-4.407c-0.68,-1.35 -1.955,-2.479 -3.46,-2.629c-0.924,-0.092 -1.869,0.183 -2.779,-0.006c-0.198,0.405 -0.265,0.944 0.064,1.251c0.33,0.307 0.747,0.518 1.037,0.863c0.396,0.469 0.501,1.116 0.519,1.729c0.082,2.845 -1.37,5.539 -1.477,8.384c-0.052,1.381 0.227,2.787 0.901,3.993c0.674,1.206 1.758,2.199 3.058,2.666c1.841,0.659 4.217,0.449 6.123,0.018z" fill="#2D294C" id="svg_408"/>
<path d="m148.76,25.747l1.141,-0.145c0,0 1.821,1.121 2.287,1.037c1.09,-0.195 1.289,-0.589 1.289,-0.589s1.077,-0.719 1.563,-0.685c6.145,0.436 5.828,4.114 6.358,6.234c0.928,3.716 -2.395,9.115 -2.395,9.115s4.126,13.236 3.942,15.548l-11.556,1.9c-3.17,0.521 -6.421,-0.151 -9.124,-1.887l0,0l1.264,-11.978l-0.833,-2.272c-1.126,-3.07 -1.414,-6.387 -0.821,-9.603c0.253,-1.368 0.523,-2.637 0.751,-3.252c0.644,-1.736 4.295,-3.189 6.134,-3.423z" fill="#6F9EDE" id="svg_409"/>
<g id="svg_410">
<g id="svg_411">
<path d="m152.026,28.777l0,0c1.696,0 3.07,-1.375 3.07,-3.07l0,-8.061c0,-0.203 -0.165,-0.367 -0.367,-0.367l-5.406,0c-0.203,0 -0.367,0.164 -0.367,0.367l0,8.061c-0.001,1.695 1.374,3.07 3.07,3.07z" fill="#EFAFC0" id="svg_412"/>
<path d="m155.096,19.621c-0.813,2.108 -2.586,5.356 -6.141,6.155l0,-8.499l5.55,0l0.59,0.259l0,2.085l0.001,0z" fill="#D98E9D" id="svg_413" opacity="0.5"/>
</g>
<path d="m147.55,8.2l1.413,0c2.809,0 5.086,2.277 5.086,5.086l0,4.812c0,2.84 -2.302,5.143 -5.143,5.143l-2.507,0c-2.173,0 -3.934,-1.761 -3.934,-3.934l0,-6.021c0,-2.809 2.277,-5.086 5.085,-5.086z" fill="#EFAFC0" id="svg_414"/>
<circle cx="154.682" cy="16.984" fill="#EFAFC0" id="svg_415" r="2.736"/>
<path d="m153.984,14.586c-0.213,0.985 -0.35,1.986 -0.41,2.991c-0.114,-0.107 -0.325,-0.155 -0.47,-0.099" fill="none" id="svg_416"/>
<path d="m145.848,19.027l2.962,-0.753c0,0 -0.472,1.61 -1.418,1.773c-0.947,0.163 -1.544,-1.02 -1.544,-1.02z" fill="#FFFFFF" id="svg_417"/>
<path d="m146.767,14.676c0,0.305 0.247,0.552 0.552,0.552c0.305,0 0.552,-0.247 0.552,-0.552c0,-0.305 -0.247,-0.552 -0.552,-0.552c-0.305,0 -0.552,0.247 -0.552,0.552z" fill="#2D294C" id="svg_418"/>
<path d="m142.905,14.676c0,0.305 0.247,0.552 0.552,0.552c0.305,0 0.552,-0.247 0.552,-0.552c0,-0.305 -0.247,-0.552 -0.552,-0.552c-0.305,0 -0.552,0.247 -0.552,0.552z" fill="#2D294C" id="svg_419"/>
<path d="m151.485,8.973c0,0 -1.112,2.88 0.312,4.687c0.434,0.551 0.784,2.299 0.931,2.984c0.153,0 0.321,-1.654 1.2,-2.207c0.304,-0.195 1.535,-0.114 1.535,-0.114s0.673,-1.691 0.818,-2.126c0.185,-0.554 0.492,-1.063 0.666,-1.62c0.581,-1.859 -0.652,-4.078 -2.537,-4.567c-0.551,-0.143 -1.122,-0.216 -1.691,-0.236c-12.074,-0.429 -10.65,5.234 -10.605,6.527c5.007,0.668 9.371,-3.328 9.371,-3.328z" fill="#2D294C" id="svg_420"/>
</g>
<path d="m163.38,57.685c0.802,-0.333 1.613,-0.804 1.996,-1.584c0.383,-0.78 -1.006,-1.349 -1.831,-1.621c-0.785,0.718 -1.993,0.582 -3.035,0.797c-0.632,0.13 -1.225,0.402 -1.799,0.696c-0.736,0.377 -1.46,0.802 -2.046,1.386c-0.586,0.584 -1.028,1.344 -1.095,2.169c-0.016,0.193 0.131,0.325 0.316,0.269c0.185,-0.057 0.299,-0.237 0.407,-0.398c0.337,-0.505 0.771,-0.945 1.27,-1.29c0.091,-0.063 0.221,0.08 0.141,0.155c-1.171,1.105 -1.214,2.089 -1.164,2.16c0.055,0.077 0.171,0.074 0.26,0.044c0.161,-0.056 0.349,-0.387 0.455,-0.52c0.45,-0.566 0.944,-0.804 0.944,-0.804s0.6,-0.157 -0.607,1.04c-0.148,0.147 -0.293,0.329 -0.291,0.538c0,0.037 0.006,0.074 0.024,0.106c0.021,0.038 0.059,0.066 0.098,0.085c0.122,0.058 0.268,0.043 0.392,-0.009c0.124,-0.052 0.231,-0.139 0.335,-0.223c0.514,-0.417 1.027,-0.834 1.541,-1.251c-0.123,0.149 -1.014,1.026 -1.082,1.207c-0.026,0.07 -0.047,0.153 -0.009,0.218c0.056,0.096 0.195,0.09 0.303,0.066c0.73,-0.16 2.161,-1.274 2.74,-1.746c0.58,-0.473 1.047,-1.203 1.737,-1.49z" fill="#EFAFC0" id="svg_421"/>
<path d="m163.052,29.686c2.636,2.75 5.273,5.5 7.909,8.25c2.443,2.548 5.045,5.473 5.038,9.003c-0.005,2.685 -1.557,5.122 -3.318,7.148c-1.249,1.437 -2.708,2.804 -4.535,3.338c-1.827,0.535 -4.086,-0.005 -5.022,-1.663c-1.015,-1.796 -0.176,-4.044 0.782,-5.871c0.958,-1.827 2.065,-3.848 1.519,-5.837c-0.388,-1.416 -1.538,-2.469 -2.585,-3.498c-2.957,-2.908 -5.618,-6.529 -6.459,-10.668c-0.357,-1.759 -0.916,-3.975 1.325,-3.962c1.979,0.012 4.113,2.473 5.346,3.76z" fill="#6F9EDE" id="svg_422"/>
<path d="m148.515,121.53l-1.375,3.425l-3.653,1.661c-1.819,0.738 -1.155,1.634 -1.155,1.634l11.019,-0.762l-1.007,-6.275l-3.829,0.317z" fill="#2D294C" id="svg_423"/>
<path d="m152.805,125.823l-3.653,1.661c-1.819,0.738 -1.155,1.634 -1.155,1.634l10.017,-0.755l0.776,-6.717l-3.943,-0.403l0.317,1.9l-2.359,2.68z" fill="#2D294C" id="svg_424"/>
</g>
<g id="svg_425">
<path d="m24.854,55.079c4.549,0.076 9.099,0.153 13.648,0.229c1.546,0.026 3.103,0.051 4.624,-0.227c3.221,-0.588 11.197,-8.695 13.435,-10.265c0.838,0.743 1.646,1.432 2.484,2.174c-2.347,4.501 -3.586,7.027 -6.704,11.032c-0.932,1.197 -1.938,2.402 -3.293,3.082c-1.054,0.528 -2.246,0.7 -3.417,0.839c-4.995,0.59 -10.047,0.696 -15.063,0.317c-0.661,-0.05 -1.359,-0.122 -1.888,-0.522c-0.461,-0.348 -0.723,-0.894 -0.967,-1.418c-1.162,-2.485 -1.699,-2.754 -2.859,-5.241z" fill="#EDA956" id="svg_426"/>
<path d="m27.581,35.701c0,0 -7.203,0.647 -9.146,7.365c-1.943,6.718 1.781,9.308 1.781,9.308l1.376,0.081l0.648,-1.295l0.486,1.295l8.579,0.971l0.567,-0.648l0.405,0.648c0,0 4.209,-1.295 3.966,-4.694c-0.243,-3.399 -0.081,-5.018 -1.133,-7.041c-1.052,-2.023 -2.914,-4.452 -2.914,-4.452l-4.615,-1.538z" fill="#563238" id="svg_427"/>
<g id="svg_428">
<path d="m35.733,93.744l4.374,34.965l0.712,0l-2.287,-36.243c0.001,0.001 -2.799,1.917 -2.799,1.278z" fill="#2D294C" id="svg_429"/>
<path d="m45.446,92.449l16.677,36.26l0.712,0l-14.589,-37.538c0,0.001 -2.8,1.917 -2.8,1.278z" fill="#2D294C" id="svg_430"/>
<path d="m23.661,92.449l-16.677,36.26l-0.712,0l14.589,-37.538c0.001,0.001 2.8,1.917 2.8,1.278z" fill="#2D294C" id="svg_431"/>
<path d="m31.755,93.744l-4.374,34.965l-0.712,0l2.287,-36.243c-0.001,0.001 2.799,1.917 2.799,1.278z" fill="#2D294C" id="svg_432"/>
<g id="svg_433">
<rect fill="#2D294C" height="0.485" id="svg_434" transform="matrix(0.4675 -0.884 0.884 0.4675 -9.1243 344.263)" width="19.406" x="230.048508" y="-67.858469"/>
</g>
<g id="svg_435">
<rect fill="#2D294C" height="22.897" id="svg_436" transform="matrix(0.6928 -0.7211 0.7211 0.6928 -43.4672 259.313)" width="0.485" x="174.469854" y="-57.134714"/>
</g>
<g id="svg_437">
<rect fill="#2D294C" height="0.486" id="svg_438" transform="matrix(0.6874 -0.7263 0.7263 0.6874 -50.5056 245.956)" width="24.505" x="141.987717" y="-45.752078"/>
</g>
<g id="svg_439">
<polygon fill="#2D294C" id="svg_440" points="30.022003173828125,107.79200744628906 20.957000732421875,92.89999389648438 21.37298583984375,92.64700317382812 30.43798828125,107.53900146484375 "/>
</g>
<path d="m18.899,55.868c0,0 10.684,-2.59 14.245,19.101l0.56,2.427c0.816,3.538 3.897,6.093 7.525,6.241l4.676,0.191c3.044,0.124 5.701,2.099 6.697,4.978l1.262,3.645l2.586,0c0,0 -0.971,5.18 -6.151,5.18c-5.18,0 -17.155,0 -17.155,0l-11.007,0c0,0 -9.065,0.648 -10.684,-11.331c-1.619,-11.979 -7.447,-31.08 7.446,-30.432z" fill="#5A8ECC" id="svg_441"/>
<path d="m59.2,124.768l2.354,2.58l3.653,1.66c1.819,0.738 1.155,1.635 1.155,1.635l-10.017,-0.755l-0.757,-3.442l3.612,-1.678z" fill="#2D294C" id="svg_442"/>
<path d="m55.342,116.368l3.858,8.4l2.354,2.58c0,0 -1.511,0.448 -3.066,0.122c-1.555,-0.326 -2.9,-1.025 -2.9,-1.025l-4.08,-6.569l-0.47,-3.327l4.304,-0.181z" fill="#EFAFC0" id="svg_443"/>
<polygon fill="#EFAFC0" id="svg_444" points="46.364990234375,119.7550048828125 43.110992431640625,122.447998046875 43.89599609375,126.93699645996094 46.701995849609375,128.0590057373047 41.989013671875,127.94700622558594 39.519989013671875,121.55099487304688 42.217010498046875,118.19599914550781 "/>
<path d="m46.702,128.059c0,0 0.991,0.013 1.331,0.391c0.34,0.377 0,0.944 0,0.944l-7.615,-0.213l-2.581,-7.406l1.683,-0.224l2.469,6.396l4.713,0.112z" fill="#2D294C" id="svg_445"/>
<g id="svg_446">
<path d="m47.267,120.77l-7.793,-3.96l1.98,-3.896c3.337,-6.565 6.933,-14.445 8.429,-18.673c-4.337,-0.627 -12.462,-1.279 -19.433,-1.635l-4.365,-0.222l0.446,-8.73l4.365,0.222c23.576,1.202 25.569,2.862 26.639,3.754c3.192,2.661 2.048,6.952 -2.774,17.706c-2.604,5.804 -5.396,11.306 -5.513,11.538l-1.981,3.896zm5.396,-25.969l0,0l0,0z" fill="#9D3542" id="svg_447"/>
</g>
<path d="m38.418,93.753l-8.349,0.463c-4.148,0 -9.259,-3.453 -9.259,-7.601l-0.124,-2.204l16.226,-1.472l1.506,10.814z" fill="#DC614B" id="svg_448"/>
<g id="svg_449">
<path d="m49.849,123.164l-0.339,-4.357c-0.68,-8.739 -2.282,-19.42 -3.618,-21.755c-1.457,-1.115 -7.727,-2.828 -13.42,-3.641l-4.326,-0.618l1.236,-8.653l4.327,0.618c4.303,0.615 14.717,2.449 18.469,6.2c3.528,3.527 5.236,16.723 6.047,27.171l0.339,4.357l-8.715,0.678zm-4.032,-26.233l0,0l0,0z" fill="#DC614B" id="svg_450"/>
</g>
<path d="m30.622,55.47c0,0 -6.79,-0.266 -7.257,-0.224c-1.32,0.117 -3.098,0.479 -3.965,0.973c-1.958,1.114 -2.138,2.935 -2.305,4.379c-0.226,1.95 4.137,11.453 4.137,11.453l-1.692,8.038c0,0 -0.26,3.501 0,5.711l17.557,-2.327l-1.423,-6.738l-1.457,-8.822c0,0 2.514,-4.672 0.89,-6.394c-3.922,-4.16 -4.485,-6.049 -4.485,-6.049z" fill="#FFCD8B" id="svg_451"/>
<g id="svg_452">
<path d="m26.036,58.4l0,0c-1.314,0 -2.378,-1.065 -2.378,-2.379l0,-10.132c0,-0.157 0.127,-0.285 0.285,-0.285l4.188,0c0.157,0 0.285,0.127 0.285,0.285l0,10.132c-0.001,1.314 -1.066,2.379 -2.38,2.379z" fill="#EFAFC0" id="svg_453"/>
<path d="m23.658,51.307c0.63,1.633 2.003,4.149 4.757,4.768l0,-10.471l-4.3,0l-0.457,0.201l0,5.502z" fill="#D98E9D" id="svg_454" opacity="0.5"/>
</g>
<path d="m30.232,37.823l-1.413,0c-2.809,0 -5.086,2.277 -5.086,5.086l0,4.812c0,2.84 2.302,5.143 5.143,5.143l0.659,0c3.193,0 5.782,-2.589 5.782,-5.782l0,-4.173c0,-2.809 -2.277,-5.086 -5.085,-5.086z" fill="#EFAFC0" id="svg_455"/>
<path d="m25.836,46.607c0,1.511 -1.225,2.736 -2.736,2.736s-2.736,-1.225 -2.736,-2.736c0,-1.511 1.225,-2.736 2.736,-2.736s2.736,1.225 2.736,2.736z" fill="#EFAFC0" id="svg_456"/>
<path d="m23.798,42.265c0.213,0.985 0.35,1.986 0.41,2.991c0.114,-0.106 0.325,-0.155 0.47,-0.099" fill="none" id="svg_457"/>
<path d="m31.935,48.65l-2.962,-0.753c0,0 0.472,1.61 1.418,1.773c0.946,0.163 1.544,-1.02 1.544,-1.02z" fill="#FFFFFF" id="svg_458"/>
<circle cx="30.464" cy="44.299" fill="#2D294C" id="svg_459" r="0.552"/>
<path d="m34.877,44.299c0,0.305 -0.247,0.552 -0.552,0.552c-0.305,0 -0.552,-0.247 -0.552,-0.552c0,-0.305 0.247,-0.552 0.552,-0.552c0.305,0 0.552,0.247 0.552,0.552z" fill="#2D294C" id="svg_460"/>
<path d="m11.452,86.3c-0.717,-5.305 -2.256,-12.005 -2.459,-17.77l0,0l5.055,14.542c1.072,3.084 3.979,5.151 7.244,5.151l3.238,0c2.861,0 5.44,1.725 6.532,4.37l2.081,5.039l-11.007,0c0,-0.001 -9.065,0.647 -10.684,-11.332z" fill="#6F9EDE" id="svg_461"/>
<path d="m23.69,89.615c-0.835,-0.239 -1.695,-0.614 -2.165,-1.344c-0.47,-0.73 0.845,-1.456 1.633,-1.82c0.862,0.623 2.047,0.35 3.107,0.443c0.643,0.057 1.263,0.258 1.867,0.485c0.774,0.29 1.542,0.63 2.191,1.142c0.649,0.513 1.176,1.218 1.337,2.029c0.038,0.19 -0.093,0.338 -0.283,0.303c-0.19,-0.035 -0.324,-0.201 -0.449,-0.349c-0.393,-0.463 -0.874,-0.851 -1.41,-1.136c-0.097,-0.052 -0.21,0.105 -0.122,0.17c1.29,0.964 1.446,1.936 1.404,2.013c-0.046,0.083 -0.161,0.093 -0.254,0.073c-0.167,-0.037 -0.391,-0.344 -0.512,-0.464c-0.512,-0.51 -1.03,-0.69 -1.03,-0.69s-0.614,-0.087 0.722,0.964c0.164,0.129 0.329,0.293 0.351,0.5c0.004,0.037 0.003,0.075 -0.012,0.109c-0.017,0.041 -0.051,0.072 -0.088,0.096c-0.114,0.072 -0.262,0.074 -0.391,0.036c-0.129,-0.038 -0.245,-0.111 -0.359,-0.184c-0.558,-0.355 -1.116,-0.71 -1.675,-1.066c0.14,0.133 1.125,0.903 1.214,1.075c0.034,0.067 0.065,0.147 0.034,0.216c-0.044,0.102 -0.183,0.112 -0.294,0.101c-0.743,-0.075 -2.293,-1.017 -2.922,-1.421c-0.628,-0.404 -1.176,-1.076 -1.894,-1.281z" fill="#EFAFC0" id="svg_462"/>
<path d="m19.326,56.368c0,0 -7.823,5.529 -9.727,14.837c-1.904,9.307 4.138,16.479 11.694,17.018l2.888,-1.432c0,0 -9.08,-9.595 -8.445,-12.768c0.635,-3.173 5.921,-10.222 5.921,-10.222s-0.521,-8.173 -2.331,-7.433z" fill="#FFCD8B" id="svg_463"/>
</g>
<path d="m24.036,44.593c0,0 7.272,-1.157 8.422,-4.988c0,0 0.668,3.521 2.86,3.303c0,0 1.247,-7.694 -6.037,-7.37c-6.637,0.295 -7.568,3.739 -7.313,8.592c0,0.001 1.62,-0.817 2.068,0.463z" fill="#563238" id="svg_464"/>
<path d="m23.442,47.902c0,0.278 -0.226,0.504 -0.504,0.504c-0.278,0 -0.504,-0.226 -0.504,-0.504c0,-0.278 0.226,-0.504 0.504,-0.504c0.278,0 0.504,0.225 0.504,0.504z" fill="#FFFFFF" id="svg_465"/>
<path d="m56.561,44.817l2.687,-2.855c0,0 -0.068,-2.583 0.408,-4.01c0.476,-1.427 0.476,-1.427 0.476,-1.427s0.748,-0.136 0.68,0.952c-0.068,1.088 0.068,2.039 0.068,2.039s3.195,-5.709 4.01,-5.981c0.816,-0.272 -0.748,3.127 -0.952,3.602c-0.204,0.476 2.651,-3.058 2.991,-2.923c0.34,0.136 -0.884,2.515 -2.039,3.806c0,0 2.787,-2.447 3.127,-2.243c0.34,0.204 -1.555,2.214 -1.555,2.214s2.708,-2.37 0.671,0.776c-0.866,1.337 -4.962,4.554 -5.437,4.758c-0.476,0.204 -2.651,3.466 -2.651,3.466l-2.484,-2.174z" fill="#EFAFC0" id="svg_466"/>
</g>
</g>
<g id="svg_467"/>
<g id="svg_480">
<g id="svg_481"/>
<g id="svg_500">
<g id="svg_502"/>
<g id="svg_517"/>
<g id="svg_532"/>
</g>
<g id="svg_545">
<g id="svg_553"/>
<g id="svg_555"/>
</g>
<g id="svg_570">
<g id="svg_572"/>
</g>
<g id="svg_591">
<g id="svg_592"/>
</g>
</g>
<g id="svg_596">
<g id="svg_597">
<g id="svg_600"/>
<g id="svg_612"/>
<g id="svg_621">
<g id="svg_625"/>
<g id="svg_627"/>
<g id="svg_629"/>
<g id="svg_631"/>
</g>
<g id="svg_633">
<g id="svg_637"/>
<g id="svg_639"/>
<g id="svg_641"/>
<g id="svg_643"/>
</g>
</g>
<g id="svg_645">
<g id="svg_646">
<g id="svg_647"/>
<g id="svg_650"/>
</g>
<g id="svg_656">
<g id="svg_657"/>
<g id="svg_660"/>
</g>
<g id="svg_666">
<g id="svg_671"/>
<g id="svg_677"/>
<g id="svg_680"/>
<g id="svg_691"/>
</g>
<g id="svg_700">
<g id="svg_704"/>
<g id="svg_706"/>
<g id="svg_709"/>
</g>
<g id="svg_730"/>
</g>
<g id="svg_735">
<g id="svg_736"/>
</g>
</g>
<g id="svg_740">
<g id="svg_742">
<g id="svg_743"/>
<g id="svg_746"/>
<g id="svg_749"/>
<g id="svg_752"/>
<g id="svg_755"/>
</g>
<g id="svg_758"/>
</g>
<g id="svg_761">
<g id="svg_762"/>
<g id="svg_765"/>
</g>
<g id="svg_768">
<g id="svg_771">
<g id="svg_772"/>
<g id="svg_774"/>
</g>
<g id="svg_776">
<g id="svg_777"/>
<g id="svg_779"/>
</g>
<g id="svg_781">
<g id="svg_782"/>
<g id="svg_784"/>
</g>
<g id="svg_786">
<g id="svg_787"/>
<g id="svg_789"/>
</g>
<g id="svg_791">
<g id="svg_792"/>
<g id="svg_794"/>
</g>
<g id="svg_796">
<g id="svg_797"/>
<g id="svg_799"/>
</g>
<g id="svg_801">
<g id="svg_802"/>
<g id="svg_804"/>
</g>
<g id="svg_806">
<g id="svg_807"/>
<g id="svg_809"/>
</g>
<g id="svg_811"/>
</g>
</g>
<g id="DESIGNED_BY_FREEPIK">
<g id="svg_814">
<g id="XMLID_319_">
<g id="XMLID_338_"/>
</g>
<g id="svg_815"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 35 KiB

+287
View File
@@ -0,0 +1,287 @@
<?xml version="1.0"?>
<svg width="120" height="240" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<g id="OBJECTS">
<g id="svg_2">
<g id="svg_3">
<g id="svg_4">
<g id="svg_5">
<g id="svg_6"/>
</g>
<g id="svg_13">
<g id="svg_14">
<g id="svg_15">
<g id="svg_17">
<g id="svg_18"/>
</g>
</g>
</g>
<g id="svg_22">
<g id="svg_23">
<g id="svg_25">
<g id="svg_26"/>
</g>
</g>
<g id="svg_29">
<g id="svg_31"/>
</g>
</g>
</g>
</g>
<g id="svg_33">
<g id="svg_34">
<g id="svg_35">
<g id="svg_37">
<g id="svg_38"/>
</g>
</g>
<g id="svg_42"/>
<g id="svg_45">
<g id="svg_46"/>
<g id="svg_49">
<g id="svg_50"/>
<g id="svg_53"/>
</g>
</g>
</g>
<g id="svg_57">
<g id="svg_59"/>
</g>
</g>
<g id="svg_63">
<g id="svg_69"/>
<g id="svg_72">
<g id="svg_78"/>
</g>
</g>
</g>
<g id="svg_87">
<g id="svg_89">
<g id="svg_90">
<g id="svg_91">
<g id="svg_94"/>
</g>
</g>
<g id="svg_98">
<g id="svg_99"/>
</g>
</g>
</g>
</g>
<g id="svg_105">
<g id="svg_106">
<g id="svg_107">
<g id="svg_108">
<path d="m61.3,141.244l-2.689,64.307l-4.824,-0.362c0,0 -5.422,-36.369 -4.115,-54.575l11.628,-9.37z" fill="#EDBC9C" id="svg_109"/>
<g id="svg_110">
<path d="m63.882,214.201c0,0 6.033,2.309 6.64,4.92l-13.121,0l-2.064,-4.069l-1.397,4.069l-1.412,0c0,0 -1.324,-10.555 1.259,-13.931l10.095,9.011z" fill="#20202A" id="svg_111"/>
<path d="m58.7,203.276l-0.089,2.275l5.837,9.767c0,0 -6.44,1.208 -8.852,-3.375c-2.412,-4.583 -1.809,-6.754 -1.809,-6.754l-0.33,-2.194l5.243,0.281z" fill="#EDBC9C" id="svg_112"/>
</g>
</g>
<g id="svg_113">
<path d="m47.796,141.244l-2.689,64.307l-4.824,-0.362c0,0 -5.422,-36.369 -4.114,-54.575l11.627,-9.37z" fill="#EDBC9C" id="svg_114"/>
<g id="svg_115">
<path d="m50.378,214.201c0,0 6.033,2.309 6.64,4.92l-13.121,0l-2.065,-4.069l-1.397,4.069l-1.412,0c0,0 -1.324,-10.555 1.259,-13.931l10.096,9.011z" fill="#20202A" id="svg_116"/>
<path d="m45.196,203.276l-0.089,2.275l5.837,9.767c0,0 -6.44,1.208 -8.852,-3.375c-2.412,-4.583 -1.809,-6.754 -1.809,-6.754l-0.33,-2.194l5.243,0.281z" fill="#EDBC9C" id="svg_117"/>
</g>
</g>
<path d="m37.759,96.171c0,0 -6.725,1.185 -10.812,43.664l-1.451,16.827c0,0 5.437,-1.917 14.885,2.343c9.449,4.26 30.029,-4.686 30.029,-4.686s-1.349,-39.087 -10.452,-59.906l-22.199,1.758z" fill="#C4B6A7" id="svg_118"/>
</g>
<g id="svg_119">
<path d="m36.29,33.629c0,0 0.423,6.627 -1.553,10.145c-1.976,3.518 -3.479,4.967 -4.006,8.788c0,0 16.576,4.945 33.177,-0.558l-6.522,-18.376l-21.096,0l0,0.001z" fill="#412424" id="svg_120"/>
<path d="m57.354,26.041c0,0 -15.531,-4.207 -19.923,3.58c-3.187,5.65 0.578,15.881 7.856,18.764c6.932,2.746 4.965,-19.842 4.965,-19.842l7.102,-2.502z" fill="#412424" id="svg_121"/>
<path d="m55.874,40.087c-0.157,1.865 1.227,3.504 3.092,3.661c1.865,0.157 3.504,-1.227 3.661,-3.092c0.157,-1.865 -1.227,-3.504 -3.092,-3.661c-1.865,-0.158 -3.504,1.227 -3.661,3.092z" fill="#DEA888" id="svg_122"/>
<path d="m54.352,49.556l-0.576,7.363c-0.197,2.586 -1.896,3.847 -4.44,3.633l0,0c-2.172,-0.183 -4.117,-1.476 -4.205,-3.653l1.464,-11.544l7.757,4.201z" fill="#EDBC9C" id="svg_123"/>
<polygon fill="#DEA888" id="svg_124" points="53.94700622558594,54.875 45.44700622558594,47.135986328125 54.425994873046875,48.803009033203125 "/>
<path d="m52.092,50.715l0,0c4.354,0.077 8,-3.3 8.182,-6.853l0.379,-8.387c0.228,-4.454 -3.118,-8.286 -7.562,-8.66l0,0c-4.549,-0.383 -9.346,2.927 -9.729,7.476l-0.231,6.749c-0.18,5.218 3.756,9.236 8.961,9.675z" fill="#EDBC9C" id="svg_125"/>
<path d="m48.166,30.423c1.575,3.798 -3.21,9.62 -3.21,9.62l-5.761,-6.925c-0.001,0 7.856,-5.383 8.971,-2.695z" fill="#412424" id="svg_126"/>
<g id="svg_127">
<path d="m39.079,40.019c-0.157,1.865 1.227,3.504 3.092,3.662c1.865,0.157 3.504,-1.227 3.661,-3.092c0.157,-1.865 -1.227,-3.504 -3.092,-3.661c-1.864,-0.159 -3.503,1.226 -3.661,3.091z" fill="#EDBC9C" id="svg_128"/>
<path d="m43.552,41.407c0,0 0.206,-2.448 -2.167,-2.26" fill="none" id="svg_129" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
</g>
<g id="svg_130">
<polyline fill="none" id="svg_131" points="55.81500244140625,41.614990234375 55.968994140625,43.125 54.83299255371094,43.170989990234375 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
<path d="m50.628,35.968c0,0 1.881,-0.164 1.902,0.627c0,0 0,0.535 -1.641,0.646c-1.641,0.11 -2.068,-0.013 -2.219,-0.516c-0.152,-0.503 1.076,-0.75 1.958,-0.757z" fill="#1F2326" id="svg_132"/>
<path d="m58.494,36.176c0,0 -1.553,-0.162 -1.678,0.581c0,0 -0.073,0.503 1.287,0.613c1.36,0.11 1.802,-0.069 1.929,-0.475c0.144,-0.46 -0.8,-0.709 -1.538,-0.719z" fill="#1F2326" id="svg_133"/>
<path d="m57.385,39.954c-0.013,0.45 0.33,0.826 0.766,0.84c0.436,0.014 0.8,-0.34 0.813,-0.79c0.013,-0.45 -0.329,-0.826 -0.766,-0.84c-0.436,-0.014 -0.8,0.34 -0.813,0.79z" fill="#1F2326" id="svg_134"/>
<path d="m50.465,39.732c-0.013,0.45 0.329,0.826 0.766,0.84c0.436,0.014 0.8,-0.34 0.813,-0.79c0.013,-0.45 -0.329,-0.826 -0.766,-0.84c-0.435,-0.014 -0.799,0.34 -0.813,0.79z" fill="#1F2326" id="svg_135"/>
<path d="m56.815,44.735l-5.726,0c0,0 0.905,1.788 3.263,1.779c2.148,-0.007 2.463,-1.779 2.463,-1.779z" fill="#FFFFFF" id="svg_136"/>
</g>
<path d="m47.205,30.423c0,0 3.424,5.851 14.408,3.798c0,0 2.67,-5.993 -4.259,-8.18c-6.93,-2.188 -14.898,1.508 -14.898,1.508l0,4.927l4.749,-2.053z" fill="#412424" id="svg_137"/>
</g>
<g id="svg_138">
<g id="svg_139">
<path d="m56.454,58.018c0,0 9.821,2.329 11.553,15.317l-6.3,5.391l-5.253,-20.708z" fill="#808DFF" id="svg_140"/>
<g id="svg_141">
<path d="m72.006,94.803c-0.177,-0.291 -0.064,2.104 -0.172,1.78c0,0 -2.925,-4.972 -7.25,-15.385c-1.106,-2.664 0.385,-10.814 3.26,-11.041c2.111,-0.167 4.618,3.77 5.121,5.827l5.243,13.186l-6.202,5.633z" fill="#EDBC9C" id="svg_142"/>
<path d="m73.62,90.323l20.437,-11.019l1.882,2.079l-16.512,16.377c-2.398,2.406 -6.473,1.593 -7.765,-1.548l0,0c-0.895,-2.177 -0.063,-4.681 1.958,-5.889z" fill="#EDBC9C" id="svg_143"/>
<path d="m101.709,78.454c1.374,1.815 0.663,4.668 -1.588,6.371c-2.251,1.703 -5.189,1.612 -6.563,-0.203c-1.374,-1.815 -0.663,-4.668 1.588,-6.371c2.251,-1.703 5.189,-1.612 6.563,0.203z" fill="#EDBC9C" id="svg_144"/>
</g>
</g>
<g id="svg_145">
<g id="svg_146">
<g id="svg_147">
<path d="m36.26,97.764c0.829,0.016 25.04,-0.28 25.04,-0.28l-1.802,-35.76l-17.901,3.048l-5.337,32.992z" fill="#FFFFFF" id="svg_148"/>
<path d="m41.597,57.649l7.183,-1.259l5.305,0.957l4.102,8.332c0,0 1.227,3.814 -5.311,4.419c-6.538,0.605 -11.078,-3.995 -12.167,-6.84c-1.09,-2.846 0.888,-5.609 0.888,-5.609z" fill="#EDBC9C" id="svg_149"/>
</g>
<g id="svg_150">
<path d="m54.085,57.347c0,0 5.376,11.324 4.043,23.017c-1.332,11.694 5.31,24.05 5.31,24.05l0,-32.265c0,0 -0.07,-13.466 -9.353,-14.802z" fill="#808DFF" id="svg_151"/>
<path d="m62.766,67.107c0,0 2.192,5.383 0.673,14.431" fill="none" id="svg_152" stroke="#5F73E8" stroke-miterlimit="10" stroke-width="0.764"/>
</g>
<path d="m41.597,57.649c0,0 16.376,20.229 -9.038,48.331l2.684,-46.65l6.354,-1.681z" fill="#808DFF" id="svg_153"/>
</g>
<g id="svg_154">
<g id="svg_155">
<g id="svg_156">
<path d="m35.286,59.315c0,0 -12.081,2.553 -12.675,26.91l14.457,-1.365l1.98,-19.807l-3.762,-5.738z" fill="#808DFF" id="svg_157"/>
<path d="m36.679,90.803c0,0 3.15,-3.086 0.389,-5.944c0,0 -10.297,-1.601 -16.298,2.167c0,0 -2.512,4.286 0.698,6.608c0,0.001 4.326,-2.831 15.211,-2.831z" fill="#9CABFF" id="svg_158"/>
<line fill="none" id="svg_159" stroke="#5F73E8" stroke-miterlimit="10" stroke-width="0.764" x1="37.068" x2="39.46" y1="84.86" y2="70.101"/>
</g>
<g id="svg_160">
<path d="m36.679,90.803l22.472,2.831l-1.249,4.419l-30.375,0.75c-3.043,0.075 -5.654,-2.153 -6.059,-5.169l0,0c0,0.001 6.728,-4.835 15.211,-2.831z" fill="#EDBC9C" id="svg_161"/>
<path d="m68.003,95.927c-0.117,2.805 -3.031,4.961 -6.508,4.816c-3.478,-0.145 -6.202,-2.536 -6.085,-5.341c0.117,-2.805 3.031,-4.961 6.508,-4.816c3.478,0.145 6.202,2.536 6.085,5.341z" fill="#EDBC9C" id="svg_162"/>
</g>
</g>
<g id="svg_163">
<path d="m37.285,93.313l23.99,0l0,-2.707l-23.99,0c-0.747,0 -1.353,0.606 -1.353,1.354l0,0c0,0.747 0.605,1.353 1.353,1.353z" fill="#1C2123" id="svg_164"/>
<g id="svg_165">
<path d="m58.117,93.313l34.442,0c1.831,0 3.438,-1.221 3.928,-2.984l6.199,-22.27c0.548,-1.969 -0.933,-3.918 -2.976,-3.918l-31.85,0c-1.222,0 -2.291,0.824 -2.601,2.006l-7.142,27.166z" fill="#2E353A" id="svg_166"/>
<path d="m78.798,79.973c0,1.661 1.347,3.007 3.008,3.007s3.007,-1.346 3.007,-3.007c0,-1.661 -1.346,-3.007 -3.007,-3.007s-3.008,1.346 -3.008,3.007z" fill="#FFFFFF" id="svg_167"/>
</g>
</g>
</g>
</g>
</g>
</g>
<g id="svg_168">
<g id="svg_170">
<g id="svg_171">
<g id="svg_172"/>
<g id="svg_174"/>
</g>
<g id="svg_178">
<g id="svg_179">
<g id="svg_180"/>
</g>
<g id="svg_183"/>
</g>
</g>
</g>
</g>
<g id="svg_186">
<g id="svg_187">
<g id="svg_188">
<g id="svg_189">
<g id="svg_191">
<g id="svg_192"/>
<g id="svg_195"/>
</g>
</g>
<g id="svg_199">
<g id="svg_201">
<g id="svg_202"/>
<g id="svg_205"/>
</g>
</g>
<g id="svg_209"/>
</g>
<g id="svg_212">
<g id="svg_213"/>
<g id="svg_216"/>
<g id="svg_219">
<g id="svg_220">
<g id="svg_222"/>
</g>
<g id="svg_225">
<g id="svg_226"/>
<g id="svg_229"/>
</g>
</g>
</g>
<g id="svg_232">
<g id="svg_233"/>
</g>
<g id="svg_239">
<g id="svg_246"/>
<g id="svg_254">
<g id="svg_255"/>
</g>
</g>
</g>
<g id="svg_259">
<g id="svg_261">
<g id="svg_262">
<g id="svg_264"/>
</g>
</g>
</g>
</g>
<g id="svg_268">
<g id="svg_269">
<g id="svg_270">
<g id="svg_271">
<g id="svg_275"/>
</g>
<g id="svg_281">
<g id="svg_282">
<g id="svg_283">
<g id="svg_284">
<g id="svg_286"/>
</g>
<g id="svg_292"/>
</g>
<g id="svg_295">
<g id="svg_296">
<g id="svg_298"/>
</g>
<g id="svg_304"/>
</g>
</g>
<g id="svg_307">
<g id="svg_308">
<g id="svg_309">
<g id="svg_310"/>
</g>
</g>
<g id="svg_315"/>
<g id="svg_318">
<g id="svg_319"/>
<g id="svg_322">
<g id="svg_323"/>
<g id="svg_326"/>
</g>
</g>
</g>
<g id="svg_329">
<g id="svg_336"/>
<g id="svg_343"/>
</g>
</g>
</g>
<g id="svg_350">
<g id="svg_351">
<g id="svg_352"/>
<g id="svg_355"/>
<g id="svg_358">
<g id="svg_360"/>
</g>
</g>
<g id="svg_363"/>
</g>
</g>
<g id="svg_368">
<g id="svg_370"/>
</g>
</g>
</g>
<g id="DESIGNED_BY_FREEPIK">
<g id="svg_373">
<g id="XMLID_319_">
<g id="XMLID_338_"/>
</g>
<g id="svg_374"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

+297
View File
@@ -0,0 +1,297 @@
<?xml version="1.0"?>
<svg width="240" height="240" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<g id="BACKGROUND"/>
<g id="OBJECTS">
<g id="svg_2">
<g id="svg_3">
<g id="svg_4">
<g id="svg_5">
<g id="svg_6"/>
</g>
<g id="svg_13">
<g id="svg_14">
<g id="svg_15">
<g id="svg_17">
<g id="svg_18"/>
</g>
</g>
</g>
<g id="svg_22">
<g id="svg_23">
<g id="svg_25">
<g id="svg_26"/>
</g>
</g>
<g id="svg_29">
<g id="svg_31"/>
</g>
</g>
</g>
</g>
<g id="svg_33">
<g id="svg_34">
<g id="svg_35">
<g id="svg_37">
<g id="svg_38"/>
</g>
</g>
<g id="svg_42"/>
<g id="svg_45">
<g id="svg_46"/>
<g id="svg_49">
<g id="svg_50"/>
<g id="svg_53"/>
</g>
</g>
</g>
<g id="svg_57">
<g id="svg_59"/>
</g>
</g>
<g id="svg_63">
<g id="svg_69"/>
<g id="svg_72">
<g id="svg_78"/>
</g>
</g>
</g>
<g id="svg_87">
<g id="svg_89">
<g id="svg_90">
<g id="svg_91">
<g id="svg_94"/>
</g>
</g>
<g id="svg_98">
<g id="svg_99"/>
</g>
</g>
</g>
</g>
<g id="svg_105">
<g id="svg_106">
<g id="svg_107">
<g id="svg_108">
<g id="svg_110"/>
</g>
<g id="svg_113">
<g id="svg_115"/>
</g>
</g>
<g id="svg_119">
<g id="svg_127"/>
<g id="svg_130"/>
</g>
<g id="svg_138">
<g id="svg_139">
<g id="svg_141"/>
</g>
<g id="svg_145">
<g id="svg_146">
<g id="svg_147"/>
<g id="svg_150"/>
</g>
<g id="svg_154">
<g id="svg_155">
<g id="svg_156"/>
<g id="svg_160"/>
</g>
<g id="svg_163">
<g id="svg_165"/>
</g>
</g>
</g>
</g>
</g>
<g id="svg_168">
<g id="svg_170">
<g id="svg_171">
<g id="svg_172"/>
<g id="svg_174"/>
</g>
<g id="svg_178">
<g id="svg_179">
<g id="svg_180"/>
</g>
<g id="svg_183"/>
</g>
</g>
</g>
</g>
<g id="svg_186">
<g id="svg_187">
<g id="svg_188">
<g id="svg_189">
<polygon fill="#EDBC9C" id="svg_190" points="105.89300537109375,190.30499267578125 107.11898803710938,208.34300231933594 115.42999267578125,207.98500061035156 115.1409912109375,189.718994140625 "/>
<g id="svg_191">
<g id="svg_192">
<path d="m106.501,206.34l-15.149,6.908c-0.992,0.498 -1.514,1.611 -1.261,2.692l0,0l26.72,0l-0.993,-9.711l-9.317,0.111z" id="svg_193"/>
<rect fill="#FFFFFF" height="2.874" id="svg_194" width="27.532" x="89.751" y="215.94"/>
</g>
<g id="svg_195">
<line fill="none" id="svg_196" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="101.835" x2="103.478" y1="208.069" y2="209.465"/>
<line fill="none" id="svg_197" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="99.631" x2="101.286" y1="209.129" y2="210.537"/>
<line fill="none" id="svg_198" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="97.419" x2="99.168" y1="210.239" y2="211.73"/>
</g>
</g>
</g>
<g id="svg_199">
<polygon fill="#EDBC9C" id="svg_200" points="132.95599365234375,190.30499267578125 134.1820068359375,208.34300231933594 142.49301147460938,207.98500061035156 142.20401000976562,189.718994140625 "/>
<g id="svg_201">
<g id="svg_202">
<path d="m133.563,206.34l-15.149,6.908c-0.992,0.498 -1.514,1.611 -1.261,2.692l0,0l26.72,0l-0.993,-9.711l-9.317,0.111z" id="svg_203"/>
<rect fill="#FFFFFF" height="2.874" id="svg_204" width="27.532" x="116.814" y="215.94"/>
</g>
<g id="svg_205">
<line fill="none" id="svg_206" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="128.898" x2="130.54" y1="208.069" y2="209.465"/>
<line fill="none" id="svg_207" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="126.694" x2="128.349" y1="209.129" y2="210.537"/>
<line fill="none" id="svg_208" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="124.482" x2="126.231" y1="210.239" y2="211.73"/>
</g>
</g>
</g>
<g id="svg_209">
<polygon fill="#C4B6A7" id="svg_210" points="110.69400024414062,105.5979995727539 142.58099365234375,108.02799987792969 145.22799682617188,197.60800170898438 129.34298706054688,197.60800170898438 123.68499755859375,130.04200744628906 116.10400390625,158.91400146484375 118.68499755859375,198.6999969482422 103.80899047851562,198.7429962158203 98.67599487304688,154.75100708007812 "/>
<line fill="none" id="svg_211" stroke="#A69382" stroke-miterlimit="10" stroke-width="0.75" x1="116.554" x2="123.685" y1="126.149" y2="130.042"/>
</g>
</g>
<g id="svg_212">
<g id="svg_213">
<polygon fill="#EBAB9D" id="svg_214" points="111.10598754882812,78.26899719238281 111.31298828125,87.06099700927734 106.32199096679688,87.125 106.69000244140625,77.66100311279297 "/>
<path d="m110.799,63.347c0,0 -6.674,4.388 -5.975,15.518l6.489,0.73l-0.514,-16.248z" fill="#808DFF" id="svg_215"/>
</g>
<g id="svg_216">
<path d="m126.881,58.394c8.026,0 17.123,2.774 17.123,2.774l-0.346,51.05l-33.9,-2.604l0.359,-44.65c0.007,-0.79 0.442,-1.514 1.136,-1.891l9.375,-4.011c0,0.001 4.615,-0.668 6.253,-0.668z" fill="#808DFF" id="svg_217"/>
<line fill="none" id="svg_218" stroke="#667AF1" stroke-miterlimit="10" stroke-width="0.764" x1="109.995" x2="109.995" y1="79.446" y2="68.69"/>
</g>
<g id="svg_219">
<g id="svg_220">
<path d="m139.987,78.372l2.181,25.251c0,0 2.408,5.078 6.667,-0.204c4.259,-5.282 0.614,-25.916 0.614,-25.916l-9.462,0.869z" fill="#EDBC9C" id="svg_221"/>
<g id="svg_222">
<path d="m137.918,80.866c0,0 -1.819,-18.64 4.081,-19.735c6.117,-1.136 9.67,10.396 10.236,18.659l-14.317,1.076z" fill="#808DFF" id="svg_223"/>
<path d="m140.187,80.695l-2.269,0.171c0,0 -0.664,-6.379 -0.265,-10.813" fill="none" id="svg_224" stroke="#667AF1" stroke-miterlimit="10" stroke-width="0.764"/>
</g>
</g>
<g id="svg_225">
<g id="svg_226">
<path d="m125.509,79.979c0,0 -1.228,-4.452 -5.684,-4.886c-4.456,-0.434 -5.767,3.064 -4.928,5.368c0.839,2.304 3.738,6.054 8.13,4.136c4.392,-1.918 2.482,-4.618 2.482,-4.618z" fill="#EDBC9C" id="svg_227"/>
<path d="m118.346,75.099c0,0 -4.376,0.502 -7.633,2.089c-0.331,0.161 -0.512,0.525 -0.464,0.89l0,0c0.065,0.493 0.531,0.831 1.02,0.74l6.039,-1.129l1.038,-2.59z" fill="#EDBC9C" id="svg_228"/>
</g>
<g id="svg_229">
<polygon fill="#EDBC9C" id="svg_230" points="143.11099243164062,104.90899658203125 123.0260009765625,84.59700012207031 125.50900268554688,79.97899627685547 145.04400634765625,92.39099884033203 "/>
<line fill="none" id="svg_231" stroke="#DEA888" stroke-miterlimit="10" stroke-width="0.764" x1="140.974" x2="145.044" y1="89.805" y2="92.391"/>
</g>
</g>
</g>
</g>
<g id="svg_232">
<g id="svg_233">
<path d="m98.081,85.09l8.447,25.074c0.29,0.862 1.151,1.399 2.053,1.28l20.53,-2.697c1.196,-0.157 1.947,-1.373 1.551,-2.513l-8.695,-25.042c-0.296,-0.853 -1.151,-1.381 -2.047,-1.264l-20.282,2.664c-1.19,0.157 -1.94,1.361 -1.557,2.498z" fill="#121314" id="svg_234"/>
<polygon fill="#121314" id="svg_235" points="98.44100952148438,83.23100280761719 97.36801147460938,84.6240005493164 129.35501098632812,109.32099914550781 130.50201416015625,107.82599639892578 "/>
<path d="m97.114,86.335l8.447,25.074c0.29,0.862 1.15,1.399 2.053,1.28l20.53,-2.697c1.196,-0.157 1.947,-1.373 1.551,-2.513l-8.695,-25.04c-0.296,-0.853 -1.151,-1.381 -2.047,-1.263l-20.282,2.664c-1.19,0.155 -1.94,1.359 -1.557,2.495z" fill="#455157" id="svg_236"/>
<path d="m109.496,94.445c0.009,1.221 1.005,2.204 2.226,2.195c1.221,-0.009 2.204,-1.005 2.195,-2.226c-0.009,-1.221 -1.005,-2.204 -2.226,-2.195c-1.221,0.008 -2.204,1.005 -2.195,2.226z" fill="#86969C" id="svg_237"/>
</g>
<path d="m98.071,93.622l1.923,5.188c0.235,0.633 0.876,1.019 1.545,0.931l0,0c1.97,-0.259 3.232,-2.23 2.642,-4.128l-0.541,-1.74c-0.433,-1.394 -1.754,-2.319 -3.212,-2.25l-1.07,0.051c-0.977,0.047 -1.627,1.03 -1.287,1.948z" fill="#EDBC9C" id="svg_238"/>
</g>
<g id="svg_239">
<path d="m119.195,26.137c0,0 19.114,-5.53 12.146,14.151l-3.095,-6.884l-9.051,-7.267z" fill="#412424" id="svg_240"/>
<path d="m111.261,33.648c0,0 -4.703,2.124 0.548,8.693l1.677,-9.011l-2.225,0.318z" fill="#412424" id="svg_241"/>
<path d="m116.028,43.543c0.332,1.727 -0.755,3.394 -2.427,3.723c-1.672,0.329 -3.296,-0.805 -3.628,-2.532c-0.332,-1.727 0.755,-3.394 2.426,-3.723c1.673,-0.33 3.298,0.804 3.629,2.532z" fill="#DEA888" id="svg_242"/>
<path d="m119.478,50.804l1.02,8.228c0.352,2.89 2.265,4.218 5.017,3.848l0,0c2.349,-0.315 4.79,-1.776 4.371,-4.31l-1.213,-12.919l-9.195,5.153z" fill="#EDBC9C" id="svg_243"/>
<path d="m120.34,57.913c0,0 9.269,-3.364 8.49,-11.668c-0.232,-2.474 -9.473,3.717 -9.473,3.717l0.983,7.951z" fill="#DEA888" id="svg_244"/>
<path d="m122.243,53.961l0,0c-5.105,0.903 -8.527,-3.188 -8.757,-5.275l-2.19,-9.681c-0.788,-4.945 2.364,-9.612 7.123,-10.548l0,0c4.872,-0.958 10.458,2.177 11.425,7.21l0.911,6.449c0.828,5.806 -2.939,10.749 -8.512,11.845z" fill="#EDBC9C" id="svg_245"/>
<g id="svg_246">
<polyline fill="none" id="svg_247" points="117.26400756835938,45.770999908447266 117.29000854492188,47.65800094604492 118.43499755859375,47.56100082397461 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5281"/>
<path d="m121.502,40.326c0,0 -1.907,0.072 -1.828,0.868c0,0 0.068,0.537 1.727,0.441c1.659,-0.096 2.072,-0.273 2.16,-0.796c0.089,-0.524 -1.173,-0.617 -2.059,-0.513z" fill="#1F2326" id="svg_248"/>
<path d="m113.909,41.295c0,0 1.537,-0.358 1.756,0.372c0,0 0.136,0.495 -1.214,0.777c-1.35,0.282 -1.816,0.157 -1.995,-0.234c-0.201,-0.444 0.715,-0.812 1.453,-0.915z" fill="#1F2326" id="svg_249"/>
<path d="m115.497,44.944c0.07,0.45 -0.227,0.87 -0.662,0.939c-0.436,0.069 -0.845,-0.24 -0.915,-0.69c-0.07,-0.45 0.226,-0.87 0.662,-0.939c0.435,-0.069 0.845,0.24 0.915,0.69z" fill="#1F2326" id="svg_250"/>
<path d="m122.407,43.851c0.07,0.45 -0.226,0.87 -0.662,0.939c-0.435,0.068 -0.845,-0.241 -0.915,-0.69c-0.07,-0.45 0.226,-0.87 0.662,-0.939c0.436,-0.069 0.845,0.24 0.915,0.69z" fill="#1F2326" id="svg_251"/>
<path d="m117.256,49.092l4.875,-0.54c0,0 -0.146,1.713 -2.692,1.714c-1.653,0.001 -2.183,-1.174 -2.183,-1.174z" fill="#FFFFFF" id="svg_252"/>
</g>
<path d="m131.754,33.022l-0.413,7.266l-1.408,1.572l0,0c-1.085,-0.37 -2.49,-0.348 -3.389,-1.572c-0.665,-0.904 -1.326,-2.593 -1.349,-3.715l-0.032,-1.564l-3.829,2.827c-0.6,-1.4 -1.279,-3.264 -1.279,-3.264s-0.913,0.065 -2.157,3.101c0,0 -9.875,2.985 -9.87,-3.036c0.005,-6.355 11.168,-8.498 11.168,-8.498l9.609,3.278l2.949,3.605z" fill="#412424" id="svg_253"/>
<g id="svg_254">
<g id="svg_255">
<path d="m135.054,42.129c0.358,1.864 -0.814,3.662 -2.618,4.017c-1.804,0.355 -3.557,-0.868 -3.915,-2.732c-0.358,-1.864 0.814,-3.662 2.618,-4.017c1.805,-0.356 3.557,0.868 3.915,2.732z" fill="#EDBC9C" id="svg_256"/>
<path d="m130.839,43.999c0,0 -0.47,-2.447 1.868,-2.507" fill="none" id="svg_257" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
</g>
</g>
<path d="m118.435,60.001l4,-1.86l7.054,0.202l4.137,0.578c0,0 1.542,6.197 -8.92,6.542c-8.365,0.276 -6.271,-5.462 -6.271,-5.462z" fill="#EDBC9C" id="svg_258"/>
</g>
</g>
<g id="svg_259">
<g id="svg_261">
<g id="svg_262">
<g id="svg_264"/>
</g>
</g>
</g>
</g>
<g id="svg_268">
<g id="svg_269">
<g id="svg_270">
<g id="svg_271">
<g id="svg_275"/>
</g>
<g id="svg_281">
<g id="svg_282">
<g id="svg_283">
<g id="svg_284">
<g id="svg_286"/>
</g>
<g id="svg_292"/>
</g>
<g id="svg_295">
<g id="svg_296">
<g id="svg_298"/>
</g>
<g id="svg_304"/>
</g>
</g>
<g id="svg_307">
<g id="svg_308">
<g id="svg_309">
<g id="svg_310"/>
</g>
</g>
<g id="svg_315"/>
<g id="svg_318">
<g id="svg_319"/>
<g id="svg_322">
<g id="svg_323"/>
<g id="svg_326"/>
</g>
</g>
</g>
<g id="svg_329">
<g id="svg_336"/>
<g id="svg_343"/>
</g>
</g>
</g>
<g id="svg_350">
<g id="svg_351">
<g id="svg_352"/>
<g id="svg_355"/>
<g id="svg_358">
<g id="svg_360"/>
</g>
</g>
<g id="svg_363"/>
</g>
</g>
<g id="svg_368">
<g id="svg_370"/>
</g>
</g>
</g>
<g id="DESIGNED_BY_FREEPIK">
<g id="svg_373">
<g id="XMLID_319_">
<g id="XMLID_338_"/>
</g>
<g id="svg_374"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

+302
View File
@@ -0,0 +1,302 @@
<?xml version="1.0"?>
<svg width="240" height="240" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<g id="BACKGROUND"/>
<g id="OBJECTS">
<g id="svg_2">
<g id="svg_3">
<g id="svg_4">
<g id="svg_5">
<g id="svg_6">
<rect fill="#F1EEEB" height="35.866" id="svg_7" width="6.506" x="140.712" y="150.904"/>
<polygon fill="#F1EEEB" id="svg_8" points="142.39498901367188,182.20001220703125 123.70401000976562,190.64801025390625 123.97000122070312,193.04299926757812 143.45999145507812,186.76901245117188 "/>
<polygon fill="#F1EEEB" id="svg_9" points="145.56900024414062,182.20001220703125 164.260986328125,190.64801025390625 163.9949951171875,193.04299926757812 144.5050048828125,186.76901245117188 "/>
</g>
<circle cx="124.552" cy="197.321" fill="#02060B" id="svg_10" r="4.467"/>
<circle cx="164.261" cy="197.321" fill="#02060B" id="svg_11" r="4.467"/>
<path d="m162.323,153.401l-37.707,0c-2.083,0 -3.771,-1.688 -3.771,-3.771l0,0c0,-2.083 1.688,-3.771 3.771,-3.771l37.707,0c2.083,0 3.771,1.688 3.771,3.771l0,0c0,2.082 -1.688,3.771 -3.771,3.771z" fill="#8292FD" id="svg_12"/>
</g>
<g id="svg_13">
<g id="svg_14">
<g id="svg_15">
<polygon fill="#B87F58" id="svg_16" points="80.69500732421875,173.71701049804688 77.38699340820312,190.06500244140625 85.1820068359375,191.42098999023438 89.40701293945312,175.04998779296875 "/>
<g id="svg_17">
<g id="svg_18">
<path d="m77.22,188.078l-15.477,3.341c-1.023,0.261 -1.734,1.189 -1.718,2.245l0,0l24.825,5.427l1.05,-9.224l-8.68,-1.789z" fill="#121314" id="svg_19"/>
<rect fill="#455157" height="2.733" id="svg_20" transform="matrix(0.9769 0.2136 -0.2136 0.9769 101.605 -57.1239)" width="26.183" x="12.597898" y="253.892476"/>
</g>
</g>
</g>
<path d="m99.456,129.387l-3.183,0.787c-6.628,1.639 -11.887,6.674 -13.814,13.224l-9.379,39.247l17.477,2.871l9.923,-37.495l10.947,-3.624l-11.971,-15.01z" fill="#A69382" id="svg_21"/>
</g>
<g id="svg_22">
<g id="svg_23">
<polygon fill="#B87F58" id="svg_24" points="115.427001953125,170.66900634765625 123.0260009765625,185.51699829101562 129.96701049804688,181.72000122070312 123.07199096679688,166.28399658203125 "/>
<g id="svg_25">
<g id="svg_26">
<path d="m121.657,184.067l-10.024,12.257c-0.638,0.841 -0.615,2.01 0.055,2.826l0,0l22.799,-11.223l-4.926,-7.868l-7.904,4.008z" fill="#121314" id="svg_27"/>
<rect fill="#455157" height="2.733" id="svg_28" transform="matrix(0.8972 -0.4417 0.4417 0.8972 -156.063 207.084)" width="26.183" x="243.41755" y="111.141864"/>
</g>
</g>
</g>
<g id="svg_29">
<path d="m119.363,123.294l-18.21,0.757l9.689,20.303l25.192,1.938c0,0 26.326,2.739 24.026,-14.856l-40.697,-8.142z" fill="#C4B6A7" id="svg_30"/>
<g id="svg_31">
<path d="m91.17,136.499l24.499,45.673l16.026,-7.099l-23.601,-51.311l-9.274,0.338c-6.254,0.201 -10.276,6.719 -7.65,12.399z" fill="#C4B6A7" id="svg_32"/>
</g>
</g>
</g>
</g>
</g>
<g id="svg_33">
<g id="svg_34">
<g id="svg_35">
<path d="m119.164,74.785c0,0 -6.882,4.837 -6.882,20.054s2.497,27.105 2.497,27.105l8.72,1.961l-4.335,-49.12z" fill="#FFFFFF" id="svg_36"/>
<g id="svg_37">
<g id="svg_38">
<path d="m103.84,110.305c0,0 -6.252,-1.651 -9.101,2.793c-1.881,2.934 -1.174,4.003 -0.345,5.388c0.457,0.764 1.732,1.74 2.619,1.812c1.948,0.158 5.366,0.815 7.866,-3.309l-1.039,-6.684z" fill="#B87F58" id="svg_39"/>
<path d="m93.889,114.774l-1.751,6.004c-0.146,0.499 0.18,1.014 0.693,1.096l0,0c0.342,0.055 0.685,-0.1 0.869,-0.394l3.313,-5.279l-3.124,-1.427z" fill="#B87F58" id="svg_40"/>
</g>
<polygon fill="#FFFFFF" id="svg_41" points="116.23199462890625,111.90301513671875 103.35299682617188,109.93301391601562 103.8070068359375,118.72100830078125 123.29800415039062,123.92001342773438 "/>
</g>
</g>
<g id="svg_42">
<path d="m119.363,78.31c0,0 -10.616,11.177 0,44.983l40.697,8.141c0,0 -1.242,-31.461 -11.867,-45.121c-10.625,-13.66 -21.59,-13.724 -21.59,-13.724l-7.24,5.721z" fill="#FFFFFF" id="svg_43"/>
<path d="m114.711,94.142c0,0 0.89,16.214 4.652,29.152" fill="none" id="svg_44" stroke="#DDDADA" stroke-miterlimit="10" stroke-width="0.9284"/>
</g>
<g id="svg_45">
<g id="svg_46">
<path d="m127.91,87.542c4.895,7.336 22.622,33.284 22.622,33.284s6.482,7.397 12.188,3.261c8.103,-5.874 -3.09,-21.8 -3.09,-21.8s-12.918,-20.839 -24.921,-27.37c-3.451,-1.877 -5.255,0.443 -5.982,3.129l-1.474,5.448c-0.375,1.384 -0.139,2.854 0.657,4.048z" fill="#FFFFFF" id="svg_47"/>
<line fill="none" id="svg_48" stroke="#DDDADA" stroke-miterlimit="10" stroke-width="0.9284" x1="132.403" x2="147.218" y1="94.142" y2="115.569"/>
</g>
<g id="svg_49">
<g id="svg_50">
<path d="m120.241,110.656c0,0 -6.252,-1.651 -9.101,2.793c-1.881,2.934 -1.174,4.003 -0.345,5.388c0.457,0.764 1.732,1.74 2.619,1.812c1.948,0.158 5.366,0.815 7.866,-3.309l-1.039,-6.684z" fill="#B87F58" id="svg_51"/>
<path d="m110.291,115.124l-1.751,6.004c-0.146,0.499 0.18,1.014 0.693,1.096l0,0c0.342,0.055 0.685,-0.1 0.869,-0.394l3.313,-5.279l-3.124,-1.427z" fill="#B87F58" id="svg_52"/>
</g>
<g id="svg_53">
<path d="m150.84,112.518c-0.645,0.161 -30.585,-1.879 -30.585,-1.879l1.129,7.257c0,0 16.813,7.16 36.492,7.289l-7.036,-12.667z" fill="#FFFFFF" id="svg_54"/>
<line fill="none" id="svg_55" stroke="#DDDADA" stroke-miterlimit="10" stroke-width="0.9284" x1="150.206" x2="120.255" y1="112.585" y2="110.639"/>
<path d="m159.607,125.308c-15.861,1.237 -37.891,-5.783 -37.891,-5.783l-0.435,-2.186" fill="none" id="svg_56" stroke="#DDDADA" stroke-miterlimit="10" stroke-width="0.9284"/>
</g>
</g>
</g>
</g>
<g id="svg_57">
<path d="m127.609,121.745l-27.943,0l0,2.017l27.943,0c0.557,0 1.009,-0.452 1.009,-1.008l0,0c0,-0.557 -0.452,-1.009 -1.009,-1.009z" fill="#121314" id="svg_58"/>
<g id="svg_59">
<path d="m63.84,89.169l17.028,0l17.173,34.593l-17.061,0c-1.656,0 -3.168,-0.943 -3.896,-2.431l-14.641,-29.923c-0.505,-1.033 0.247,-2.239 1.397,-2.239z" fill="#455157" id="svg_60"/>
<path d="m80.869,89.169l0.795,0c1.405,0 2.69,0.794 3.319,2.051l16.271,32.542l-3.212,0l-17.173,-34.593z" fill="#121314" id="svg_61"/>
<path d="m79.763,105.69c0.708,1.691 0.451,3.409 -0.574,3.837c-1.024,0.429 -2.428,-0.595 -3.135,-2.285c-0.707,-1.691 -0.451,-3.409 0.574,-3.837c1.024,-0.429 2.427,0.594 3.135,2.285z" fill="#86969C" id="svg_62"/>
</g>
</g>
</g>
<g id="svg_63">
<path d="m107.855,63.708c1.012,1.758 0.408,4.003 -1.35,5.015c-1.758,1.012 -4.003,0.408 -5.015,-1.35c-1.012,-1.758 -0.408,-4.003 1.35,-5.015c1.758,-1.013 4.003,-0.408 5.015,1.35z" fill="#A06945" id="svg_64"/>
<path d="m110.405,67.704l5.565,9.936c1.811,3.205 4.138,2.184 7.279,0.376l0,0c2.681,-1.544 4.523,-3.65 3.324,-6.501l-6.221,-12.162l-9.947,8.351z" fill="#B87F58" id="svg_65"/>
<path d="m115.492,76.787c0,0 4.764,-3.52 5.57,-12.784c0.211,-2.425 -9.144,6.441 -9.144,6.441l3.574,6.343z" fill="#A56C4B" id="svg_66"/>
<path d="m117.339,70.892l-3.032,1.746c-3.342,1.924 -7.613,0.699 -9.426,-2.705l-5.009,-9.402c-2.273,-4.266 -0.752,-9.565 3.436,-11.976l0,0c4.288,-2.469 10.517,-1.427 12.986,2.861l4.314,8.438c2.026,3.963 0.588,8.818 -3.269,11.038z" fill="#B87F58" id="svg_67"/>
<path d="m117.671,59.352l-5.752,-7.429c0,0 -6.768,5.181 -12.864,6.437l-0.948,-1.194c-1.432,-2.487 -0.477,-5.667 2.089,-6.953l10.379,-5.192c7.407,-5.069 8.61,8.96 8.61,8.96l-1.514,5.371z" fill="#1F2326" id="svg_68"/>
<g id="svg_69">
<path d="m123.323,54.801c1.012,1.758 0.408,4.003 -1.35,5.015c-1.758,1.012 -4.003,0.408 -5.015,-1.35c-1.012,-1.758 -0.408,-4.003 1.35,-5.015c1.758,-1.012 4.003,-0.408 5.015,1.35z" fill="#B87F58" id="svg_70"/>
<path d="m119.676,58.548c0,0 -1.329,-2.308 1.087,-3.215" fill="none" id="svg_71" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.4844"/>
</g>
<g id="svg_72">
<polyline fill="none" id="svg_73" points="107.80099487304688,66.94198608398438 108.35000610351562,68.36099243164062 109.3699951171875,67.77301025390625 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.4844"/>
<path d="m109.488,59.892c0,0 -1.867,0.729 -1.377,1.672c0,0 0.345,0.632 1.953,-0.043c1.608,-0.675 1.928,-1.03 1.746,-1.697c-0.182,-0.667 -1.491,-0.356 -2.322,0.068z" fill="#1F2326" id="svg_74"/>
<path d="m102.882,63.629c0,0 1.35,-0.953 1.946,-0.137c0,0 0.392,0.558 -0.811,1.355c-1.202,0.797 -1.732,0.802 -2.113,0.385c-0.431,-0.472 0.293,-1.229 0.978,-1.603z" fill="#1F2326" id="svg_75"/>
<path d="m105.237,65.74c0.173,0.337 0.04,0.75 -0.297,0.923c-0.337,0.173 -0.75,0.04 -0.923,-0.297c-0.173,-0.337 -0.04,-0.75 0.297,-0.923c0.336,-0.173 0.75,-0.04 0.923,0.297z" fill="#1F2326" id="svg_76"/>
<path d="m111.555,62.703c0.173,0.337 0.04,0.75 -0.297,0.923c-0.337,0.173 -0.75,0.04 -0.923,-0.297c-0.173,-0.337 -0.04,-0.75 0.297,-0.923c0.337,-0.173 0.75,-0.04 0.923,0.297z" fill="#1F2326" id="svg_77"/>
<g id="svg_78">
<path d="m104.782,63.321c1.772,1.22 2.347,3.639 1.284,5.404c-1.064,1.765 -3.363,2.206 -5.135,0.987c-1.772,-1.22 -2.347,-3.639 -1.284,-5.404s3.363,-2.207 5.135,-0.987z" fill="none" id="svg_79" stroke="#FFFFFF" stroke-miterlimit="10" stroke-width="0.5379"/>
<path d="m115.137,60.479c1.06,2.067 0.244,4.603 -1.823,5.663s-4.603,0.244 -5.663,-1.823c-1.06,-2.067 -0.244,-4.602 1.823,-5.663c2.067,-1.06 4.602,-0.244 5.663,1.823z" fill="none" id="svg_80" stroke="#FFFFFF" stroke-miterlimit="10" stroke-width="0.5583"/>
<line fill="none" id="svg_81" stroke="#FFFFFF" stroke-miterlimit="10" stroke-width="0.5583" x1="106.288" x2="107.728" y1="65.207" y2="64.469"/>
<line fill="none" id="svg_82" stroke="#FFFFFF" stroke-miterlimit="10" stroke-width="0.5583" x1="115.094" x2="116.467" y1="60.254" y2="57.798"/>
</g>
<path d="m108.712,69.837l3.285,-1.825c0,0 0.557,1.217 -0.95,2.076c-1.507,0.859 -2.335,-0.251 -2.335,-0.251z" fill="#FFFFFF" id="svg_83"/>
</g>
<path d="m109.13,51.9c0,0 1.992,4.043 5.689,2.133c4.311,-2.228 -1.543,-4.731 -1.543,-4.731l-4.146,2.598z" fill="#1F2326" id="svg_84"/>
<path d="m99.054,58.361c0,0 -3.777,0.019 -4.245,-3.688c-0.467,-3.707 3.542,-4.175 3.542,-4.175s-0.946,-2.081 0.951,-3.418c1.897,-1.337 4.607,0.405 4.607,0.405s7.908,-5.598 10.438,-3.469c2.53,2.129 -8.964,8.538 -8.964,8.538l-6.329,5.807z" fill="#1F2326" id="svg_85"/>
<path d="m126.697,74.575c0,0 -2.129,4.731 -7.66,9.867c0,0 -2.312,-3.402 -2.767,-7.287l10.427,-2.58z" fill="#B87F58" id="svg_86"/>
</g>
</g>
<g id="svg_87">
<g id="svg_89">
<g id="svg_90">
<g id="svg_91">
<g id="svg_94"/>
</g>
</g>
<g id="svg_98">
<g id="svg_99"/>
</g>
</g>
</g>
</g>
<g id="svg_105">
<g id="svg_106">
<g id="svg_107">
<g id="svg_108">
<g id="svg_110"/>
</g>
<g id="svg_113">
<g id="svg_115"/>
</g>
</g>
<g id="svg_119">
<g id="svg_127"/>
<g id="svg_130"/>
</g>
<g id="svg_138">
<g id="svg_139">
<g id="svg_141"/>
</g>
<g id="svg_145">
<g id="svg_146">
<g id="svg_147"/>
<g id="svg_150"/>
</g>
<g id="svg_154">
<g id="svg_155">
<g id="svg_156"/>
<g id="svg_160"/>
</g>
<g id="svg_163">
<g id="svg_165"/>
</g>
</g>
</g>
</g>
</g>
<g id="svg_168">
<g id="svg_170">
<g id="svg_171">
<g id="svg_172"/>
<g id="svg_174"/>
</g>
<g id="svg_178">
<g id="svg_179">
<g id="svg_180"/>
</g>
<g id="svg_183"/>
</g>
</g>
</g>
</g>
<g id="svg_186">
<g id="svg_187">
<g id="svg_188">
<g id="svg_189">
<g id="svg_191">
<g id="svg_192"/>
<g id="svg_195"/>
</g>
</g>
<g id="svg_199">
<g id="svg_201">
<g id="svg_202"/>
<g id="svg_205"/>
</g>
</g>
<g id="svg_209"/>
</g>
<g id="svg_212">
<g id="svg_213"/>
<g id="svg_216"/>
<g id="svg_219">
<g id="svg_220">
<g id="svg_222"/>
</g>
<g id="svg_225">
<g id="svg_226"/>
<g id="svg_229"/>
</g>
</g>
</g>
<g id="svg_232">
<g id="svg_233"/>
</g>
<g id="svg_239">
<g id="svg_246"/>
<g id="svg_254">
<g id="svg_255"/>
</g>
</g>
</g>
<g id="svg_259">
<g id="svg_261">
<g id="svg_262">
<g id="svg_264"/>
</g>
</g>
</g>
</g>
<g id="svg_268">
<g id="svg_269">
<g id="svg_270">
<g id="svg_271">
<g id="svg_275"/>
</g>
<g id="svg_281">
<g id="svg_282">
<g id="svg_283">
<g id="svg_284">
<g id="svg_286"/>
</g>
<g id="svg_292"/>
</g>
<g id="svg_295">
<g id="svg_296">
<g id="svg_298"/>
</g>
<g id="svg_304"/>
</g>
</g>
<g id="svg_307">
<g id="svg_308">
<g id="svg_309">
<g id="svg_310"/>
</g>
</g>
<g id="svg_315"/>
<g id="svg_318">
<g id="svg_319"/>
<g id="svg_322">
<g id="svg_323"/>
<g id="svg_326"/>
</g>
</g>
</g>
<g id="svg_329">
<g id="svg_336"/>
<g id="svg_343"/>
</g>
</g>
</g>
<g id="svg_350">
<g id="svg_351">
<g id="svg_352"/>
<g id="svg_355"/>
<g id="svg_358">
<g id="svg_360"/>
</g>
</g>
<g id="svg_363"/>
</g>
</g>
<g id="svg_368">
<g id="svg_370"/>
</g>
</g>
</g>
<g id="DESIGNED_BY_FREEPIK">
<g id="svg_373">
<g id="XMLID_319_">
<g id="XMLID_338_"/>
</g>
<g id="svg_374"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

+80 -6
View File
@@ -1,15 +1,89 @@
<template>
<div></div>
<img style="display: none;" :src="devCookiePath" v-if="!isProduction" />
<v-app>
<router-view />
</v-app>
</template>
<script>
import { useTheme } from 'vuetify';
import { register } from 'register-service-worker';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/setting.js';
import { useUserStore } from '@/stores/user.js';
import { useTokensStore } from '@/stores/token.js';
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
import { isProduction } from '@/lib/version.js';
import { loadMapAssets } from '@/lib/map/index.js';
import { getSystemTheme } from '@/lib/ui.js';
export default {
created() {
if (process.env.NODE_ENV === 'production') {
window.location.replace('../mobile/');
} else {
window.location.replace('../mobile.html');
data() {
return {
isProduction: isProduction(),
devCookiePath: isProduction() ? '' : '/dev/cookies'
}
},
computed: {
...mapStores(useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore),
},
created() {
const self = this;
const theme = useTheme();
if (self.settingsStore.appSettings.theme === 'light') {
theme.global.name.value = 'light';
} else if (self.settingsStore.appSettings.theme === 'dark') {
theme.global.name.value = 'dark';
} else {
theme.global.name.value = getSystemTheme();
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
if (self.settingsStore.appSettings.theme === 'auto') {
if (e.matches) {
theme.global.name.value = 'dark';
} else {
theme.global.name.value = 'light';
}
}
});
let localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage, self.settingsStore.appSettings.timeZone);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
if (self.$user.isUserLogined()) {
if (!self.settingsStore.appSettings.applicationLock) {
// refresh token if user is logined
self.tokensStore.refreshTokenAndRevokeOldToken().then(response => {
if (response.user) {
localeDefaultSettings = self.$locale.setLanguage(response.user.language);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
}
});
// auto refresh exchange rates data
if (self.settingsStore.appSettings.autoUpdateExchangeRatesData) {
self.exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
}
}
}
if (isProduction()) {
register('./sw.js', {
registrationOptions: {
scope: './'
}
});
}
},
mounted() {
document.addEventListener('DOMContentLoaded', () => {
const languageInfo = this.$locale.getCurrentLanguageInfo();
loadMapAssets(languageInfo ? languageInfo.code : null);
});
}
}
</script>
+43 -526
View File
@@ -9,20 +9,31 @@
import { f7ready } from 'framework7-vue';
import routes from './router/mobile.js';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/setting.js';
import { useUserStore } from '@/stores/user.js';
import { useTokensStore } from '@/stores/token.js';
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
import { isProduction } from '@/lib/version.js';
import { getTheme, isEnableAnimate } from '@/lib/settings.js';
import { loadMapAssets } from '@/lib/map/index.js';
import { isModalShowing, setAppFontSize } from '@/lib/ui.mobile.js';
export default {
data() {
const self = this;
let darkMode = 'auto';
if (self.$settings.getTheme() === 'light') {
if (getTheme() === 'light') {
darkMode = false;
} else if (self.$settings.getTheme() === 'dark') {
} else if (getTheme() === 'dark') {
darkMode = true;
}
return {
isProduction: self.$settings.isProduction(),
devCookiePath: self.$settings.isProduction() ? '' : '/dev/cookies',
isProduction: isProduction(),
devCookiePath: isProduction() ? '' : '/dev/cookies',
f7params: {
name: 'ezBookkeeping',
theme: 'ios',
@@ -36,31 +47,31 @@ export default {
tapHold: true
},
serviceWorker: {
path: self.$settings.isProduction() ? './sw.js' : undefined,
path: isProduction() ? './sw.js' : undefined,
scope: './',
},
actions: {
animate: self.$settings.isEnableAnimate(),
animate: isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
dialog: {
animate: self.$settings.isEnableAnimate(),
animate: isEnableAnimate(),
backdrop: true
},
popover: {
animate: self.$settings.isEnableAnimate(),
animate: isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
popup: {
animate: self.$settings.isEnableAnimate(),
animate: isEnableAnimate(),
backdrop: true,
closeOnEscape: true,
swipeToClose: true
},
sheet: {
animate: self.$settings.isEnableAnimate(),
animate: isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
@@ -68,7 +79,7 @@ export default {
routableModals: false
},
view: {
animate: self.$settings.isEnableAnimate(),
animate: isEnableAnimate(),
browserHistory: !self.isiOSHomeScreenMode(),
browserHistoryInitialMatch: true,
browserHistoryAnimate: false,
@@ -81,26 +92,35 @@ export default {
hasBackdrop: undefined
}
},
computed: {
...mapStores(useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore),
},
created() {
const self = this;
let localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage, self.settingsStore.appSettings.timeZone);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
if (self.$user.isUserLogined()) {
if (!self.$settings.isEnableApplicationLock()) {
if (!self.settingsStore.appSettings.applicationLock) {
// refresh token if user is logined
self.$store.dispatch('refreshTokenAndRevokeOldToken').then(response => {
if (response.user && response.user.language) {
self.$locale.setLanguage(response.user.language);
self.tokensStore.refreshTokenAndRevokeOldToken().then(response => {
if (response.user) {
localeDefaultSettings = self.$locale.setLanguage(response.user.language);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
}
});
// auto refresh exchange rates data
if (self.$settings.isAutoUpdateExchangeRatesData()) {
self.$store.dispatch('getLatestExchangeRates', { silent: true, force: false });
if (self.settingsStore.appSettings.autoUpdateExchangeRatesData) {
self.exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
}
}
}
},
mounted() {
setAppFontSize(this.settingsStore.appSettings.fontSize);
f7ready((f7) => {
this.isDarkMode = f7.darkMode;
this.setThemeColorMeta(f7.darkMode);
@@ -117,7 +137,7 @@ export default {
f7.on('sheetClose', (event) => this.onBackdropChanged(event));
f7.on('pageBeforeOut', () => {
if (this.$ui.isModalShowing()) {
if (isModalShowing()) {
f7.actions.close('.actions-modal.modal-in', false);
f7.dialog.close('.dialog.modal-in', false);
f7.popover.close('.popover.modal-in', false);
@@ -131,6 +151,11 @@ export default {
this.setThemeColorMeta(isDarkMode);
});
});
document.addEventListener('DOMContentLoaded', () => {
const languageInfo = this.$locale.getCurrentLanguageInfo();
loadMapAssets(languageInfo ? languageInfo.code : null);
});
},
methods: {
isiOSHomeScreenMode() {
@@ -174,511 +199,3 @@ export default {
}
}
</script>
<style>
/** Global style **/
html, body {
position: fixed;
}
body {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
/** Common class **/
.no-right-border {
border-right: 0;
}
.no-bottom-border {
border-bottom: 0;
}
.work-break-all {
word-break: break-all;
}
.full-line {
width: 100%;
}
.smaller {
font-size: 10px;
}
.readonly {
pointer-events: none !important;
}
.skeleton-text {
pointer-events: none !important;
}
.segmented.readonly .button:not(.button-active) > span,
.list.readonly .item-content .item-title.item-label,
.list.readonly .item-content .item-title > .item-header {
opacity: 0.55 !important;
}
/** Replacing the default style of framework7 **/
:root {
--f7-popup-push-offset: 5px;
--f7-color-gray: #8e8e93;
--f7-color-gray-rgb: 142, 142, 147;
--f7-color-gray-shade: #79797f;
--f7-color-gray-tint: #a3a3a7;
--default-icon-color: var(--f7-text-color);
}
:root .dark {
--default-icon-color: var(--f7-text-color);
}
.color-gray {
--f7-theme-color: var(--f7-color-gray);
--f7-theme-color-rgb: var(--f7-color-gray-rgb);
--f7-theme-color-shade: var(--f7-color-gray-shade);
--f7-theme-color-tint: var(--f7-color-gray-tint);
}
.ios .dark, .ios.dark {
--f7-list-item-header-text-color: inherit !important;
}
i.icon.la, i.icon.las, i.icon.lab {
font-size: 28px;
}
.chip.chip-placeholder {
border: 0;
}
/** Replacing the default style of @vuepic/vue-datepicker **/
.dp__theme_light {
--dp-primary-color: #c67e48;
}
.dp__theme_dark {
--dp-primary-color: #c67e48;
}
/** Common class for replacing the default style of framework7 **/
.navbar .navbar-compact-icons.right a + a {
margin-left: 0;
}
.toolbar-item-auto-size .toolbar-inner {
padding-left: 16px;
padding-right: 16px;
}
.toolbar-item-auto-size .toolbar-inner > .link {
width: auto;
}
.tabbar-primary-link,
.tabbar-item-changed {
color: var(--f7-theme-color);
}
.tabbar-text-with-ellipsis > span {
display: block;
width: 100%;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.block-title .accordion-item-toggle .icon {
color: var(--f7-list-chevron-icon-color);
font-size: var(--f7-list-chevron-icon-font-size);
font-weight: bolder;
}
.list-item-media-valign-middle .item-media {
align-self: normal !important;
}
.list-item-no-item-after .item-after {
display: none;
}
.list-item-with-header-and-title .item-content .item-header {
margin-bottom: 11px;
}
.list-item-with-header-and-title.list-item-title-hide-overflow .item-content .list-item-custom-title {
overflow: hidden;
text-overflow: ellipsis;
}
.list .item-content .input.list-title-input {
margin-top: calc(-1 * var(--f7-list-item-padding-vertical));
margin-bottom: calc(-1 * var(--f7-list-item-padding-vertical));
}
.list .item-content .list-item-valign-middle {
align-self: center;
}
.list .item-content .list-item-showing {
color: rgba(0, 0, 0, 0.2);
font-size: 16px;
font-weight: bold;
}
.dark .list .item-content .list-item-showing {
color: rgba(255, 255, 255, 0.2);
}
.accordion-item.list-item-checked > .item-link > .item-content .item-title {
font-weight: bold;
}
.list.list-dividers li.has-child-list-item .item-inner:after {
content: '';
position: absolute;
background-color: var(--f7-list-item-border-color);
display: block !important;
z-index: 15;
top: auto;
right: auto;
bottom: 0;
left: 0;
height: 1px;
width: 100%;
transform-origin: 50% 100%;
transform: scaleY(calc(1 / var(--f7-device-pixel-ratio)));
}
.list.list-dividers li.list-group-title:first-child,
.list.list-dividers li.list-group-title.actual-first-child {
border-radius: var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius) 0 0;
}
.list.list-dividers li.list-group-title:first-child:before,
.list.list-dividers li.list-group-title.actual-first-child:before {
background-color: transparent;
}
.list.list-dividers li:last-child > .swipeout-content > .item-content > .item-inner:after,
.list.list-dividers li:last-child > .swipeout-content > .item-link > .item-content > .item-inner:after,
.list.list-dividers li.actual-last-child > .swipeout-content > .item-content > .item-inner:after,
.list.list-dividers li.actual-last-child > .swipeout-content > .item-link > .item-content > .item-inner:after {
background-color: transparent;
}
.list.inset li.list-group-title:first-child > a.button {
border-radius: var(--f7-button-border-radius);
}
.list.inset li.swipeout.actual-first-child,
.list.inset li.actual-first-child > a {
border-radius: var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius) 0 0;
}
.list.inset li.swipeout.actual-last-child,
.list.inset li.actual-last-child > a {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
.list.inset li.swipeout.actual-first-child.actual-last-child,
.list.inset li.actual-first-child.actual-last-child > a {
border-radius: var(--f7-list-inset-border-radius);
}
.list.inset.list-has-group-title li.swipeout.actual-first-child,
.list.inset.list-has-group-title li.actual-first-child > a {
border-radius: 0;
}
.list.inset.list-has-group-title li.swipeout:first-child:last-child,
.list.inset.list-has-group-title li:first-child:last-child > a,
.list.inset.list-has-group-title li.swipeout.actual-first-child.actual-last-child,
.list.inset.list-has-group-title li.actual-first-child.actual-last-child > a {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
.accordion-item .block-title+.accordion-item-content .list.inset li.swipeout:first-child:not(:last-child),
.accordion-item .block-title+.accordion-item-content .list.inset li:first-child:not(:last-child) > a {
border-radius: 0;
}
.accordion-item .block-title+.accordion-item-content .list.inset li.swipeout:first-child:last-child,
.accordion-item .block-title+.accordion-item-content .list.inset li:first-child:last-child > a,
.accordion-item .block-title+.accordion-item-content .list.inset li.swipeout.actual-last-child:first-child,
.accordion-item .block-title+.accordion-item-content .list.inset li.actual-last-child:first-child > a {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
.list .item-content .list-item-checked-icon {
font-size: 20px;
color: var(--f7-radio-active-color, var(--f7-theme-color));
margin-right: calc(var(--f7-list-item-media-margin) + var(--f7-checkbox-extra-margin));
}
.list .item-content > .item-inner > .item-after .list-item-checked-icon {
margin-right: 0;
}
.list li.no-margin .item-content.item-input {
margin: 0;
}
.ebk-list-item-error-info div.item-footer {
color: var(--f7-input-error-text-color)
}
.skeleton-text .list-item-toggle .item-after {
height: var(--f7-toggle-height);
}
.skeleton-text .list-item-toggle .item-after > span {
line-height: var(--f7-toggle-height);
font-size: var(--f7-toggle-height);
}
.no-sortable > .sortable-handler {
display: none;
}
.card-header-content {
opacity: 0.6;
}
.icon-after-text {
margin-left: 6px;
}
.badge.right-bottom-icon {
margin-left: -12px;
margin-top: 14px;
width: 16px;
height: 16px;
}
.badge.right-bottom-icon > .icon {
font-size: 13px;
width: 13px;
height: 13px;
}
/** Swipe handler **/
.swipe-handler {
height: 26px;
position: absolute;
left: 0;
width: 100%;
top: 0;
cursor: pointer;
}
.swipe-handler:after {
content: "";
width: 36px;
height: 6px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -18px;
margin-top: -8px;
border-radius: 3px;
background: #666
}
/** list-item-with-multi-item for framework7 **/
.list-item-with-multi-item .item-content,
.list-item-with-multi-item .item-inner {
padding: 0;
}
.list-item-with-multi-item .item-inner > div {
width: 100%;
}
.list-item-with-multi-item > .item-content > .item-inner:after {
background-color: transparent;
}
.list-item-with-multi-item .list-item-subitem:first-child .item-content {
padding-left: calc(var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-left));
}
.list-item-with-multi-item .list-item-subitem .item-inner {
display: block;
width: 100%;
padding-left: calc(var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-left));
padding-top: var(--f7-list-item-padding-vertical);
padding-bottom: var(--f7-list-item-padding-vertical);
}
.list-item-with-multi-item .list-item-subitem:first-child .item-inner {
padding-left: 0;
}
/** Combination list for framework7 **/
.combination-list-wrapper {
margin: 0;
padding: 0;
}
.combination-list-wrapper .block-title {
margin-top: 0;
margin-bottom: 0;
}
.combination-list-wrapper .list.combination-list-header {
margin: 0;
}
.combination-list-wrapper .list.combination-list-header .item-title {
width: 100%;
display: flex;
}
.combination-list-wrapper .list.combination-list-header > ul {
background-color: var(--f7-list-group-title-bg-color);
}
.combination-list-wrapper .list.combination-list-header.combination-list-opened > ul {
border-radius: var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius) 0 0;
}
.combination-list-wrapper .list.combination-list-header.combination-list-closed > ul {
border-radius: var(--f7-list-inset-border-radius);
}
.combination-list-wrapper .list.combination-list-header .combination-list-chevron-icon {
margin-left: auto;
}
.combination-list-wrapper .list.combination-list-content.inset > ul {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
/** Nested List item for framework7 **/
.nested-list-item .item-title {
width: 100%;
}
.nested-list-item.has-child-list-item > .swipeout-content > .item-content > .item-inner,
.nested-list-item.has-child-list-item > .swipeout-content > .item-link > .item-content > .item-inner {
padding-top: 0;
padding-bottom: 0;
}
.nested-list-item.has-child-list-item .nested-list-item-child .item-inner {
padding-bottom: var(--f7-list-item-padding-vertical);
}
.nested-list-item.has-child-list-item .item-link.active-state {
background-color: transparent;
}
.nested-list-item.has-child-list-item .item-link .item-inner {
padding-right: 0;
}
.nested-list-item.has-child-list-item .item-link .item-inner:before {
color: transparent;
}
.nested-list-item.has-child-list-item .item-link.active-state .item-inner .nested-list-item-child .item-link.active-state {
background-color: var(--f7-list-link-pressed-bg-color);
}
.nested-list-item.has-child-list-item .item-link .item-inner .nested-list-item-child .item-link .item-inner {
padding-right: calc(var(--f7-list-chevron-icon-area) + var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-right));
}
.nested-list-item.has-child-list-item .item-link .item-inner .nested-list-item-child .item-link .item-inner:before {
color: var(--f7-list-chevron-icon-color);
}
.nested-list-item .nested-list-item-title {
align-self: center;
margin-right: var(--f7-list-item-media-margin);
overflow: hidden;
text-overflow: ellipsis;
}
.nested-list-item.has-child-list-item .nested-list-item-title {
margin-left: var(--f7-list-item-media-margin);
}
.nested-list-item.has-child-list-item > .swipeout-content > .item-content > .item-inner:after,
.nested-list-item.has-child-list-item > .swipeout-content > .item-link > .item-content > .item-inner:after {
background-color: transparent;
}
.list.inset li.nested-list-item.has-child-list-item .item-inner li.nested-list-item-child,
.list.inset li.nested-list-item.has-child-list-item .item-inner li.nested-list-item-child > a {
border-radius: 0;
}
.list.list-dividers li.nested-list-item.has-child-list-item .item-inner .nested-list-item-child .item-inner:after {
content: '';
position: absolute;
background-color: var(--f7-list-item-border-color);
display: block !important;
z-index: 15;
top: auto;
right: auto;
bottom: 0;
left: 0;
height: 1px;
width: 100%;
transform-origin: 50% 100%;
transform: scaleY(calc(1 / var(--f7-device-pixel-ratio)));
}
.list.list-dividers li.nested-list-item.has-child-list-item:last-child .item-inner .nested-list-item-child:last-child .item-inner:after,
.list.list-dividers li.nested-list-item.has-child-list-item.actual-last-child .item-inner .nested-list-item-child.actual-last-child .item-inner:after {
background-color: transparent;
}
.sortable-enabled .nested-list-item .nested-list-item-child .item-inner {
padding-right: var(--f7-safe-area-right) !important;
}
/** Fix @vuepic/vue-datepicker style issue **/
.dp__main.dp__flex_display {
flex-direction: column
}
.dp__main .dp__preset_range {
white-space: inherit;
}
.dp__main .dp__menu_inner {
padding-top: 0;
padding-bottom: 0;
}
.dp__main .dp__menu_inner .dp__month_year_row > button {
width: inherit;
}
.dp__main .dp__menu_inner .dp__month_year_row > button.dp__button {
width: 100%;
}
.dp__main .dp__menu_inner .dp__month_year_row .dp__month_year_wrap > button {
line-height: inherit;
}
.dp__main .dp__calendar .dp__calendar_item {
display: flex;
justify-content: center;
flex: 1;
}
.dp__main .dp__calendar .dp__calendar_item > .dp__cell_inner {
width: 100%;
}
</style>
+119
View File
@@ -0,0 +1,119 @@
<template>
<div ref="mapContainer" style="width: 100%" :class="mapClass" :style="finalMapStyle"></div>
<slot name="error-title"
:mapSupported="mapSupported" :mapDependencyLoaded="mapDependencyLoaded"
v-if="!mapSupported || !mapDependencyLoaded"></slot>
<slot name="error-content"
:mapSupported="mapSupported" :mapDependencyLoaded="mapDependencyLoaded"
v-if="!mapSupported || !mapDependencyLoaded"></slot>
</template>
<script>
import {
copyObjectTo
} from '@/lib/common.js';
import {
createMapHolder,
initMapInstance,
setMapCenterTo,
setMapCenterMarker,
removeMapCenterMarker
} from '@/lib/map/index.js';
export default {
props: [
'height',
'mapClass',
'mapStyle',
'geoLocation'
],
expose: [
'init'
],
data() {
this.mapHolder = createMapHolder();
return {
mapSupported: !!this.mapHolder,
mapDependencyLoaded: this.mapHolder && this.mapHolder.dependencyLoaded,
mapInited: false,
initCenter: {
latitude: 0,
longitude: 0,
},
zoomLevel: 1
}
},
computed: {
finalMapStyle() {
const styles = copyObjectTo(this.mapStyle, {});
if (this.height) {
styles.height = this.height;
}
if (!this.mapSupported || !this.mapDependencyLoaded) {
styles.height = '0';
}
return styles;
}
},
methods: {
init() {
let isFirstInit = false;
let centerChanged = false;
if (!this.mapSupported || !this.mapDependencyLoaded) {
return;
}
if (this.geoLocation && (this.geoLocation.longitude || this.geoLocation.latitude)) {
if (this.initCenter.latitude !== this.geoLocation.latitude || this.initCenter.longitude !== this.geoLocation.longitude) {
this.initCenter.latitude = this.geoLocation.latitude;
this.initCenter.longitude = this.geoLocation.longitude;
this.zoomLevel = this.mapHolder.defaultZoomLevel;
centerChanged = true;
}
} else if (!this.geoLocation || (!this.geoLocation.longitude && !this.geoLocation.latitude)) {
if (this.initCenter.latitude || this.initCenter.longitude) {
this.initCenter.latitude = 0;
this.initCenter.longitude = 0;
this.zoomLevel = this.mapHolder.minZoomLevel;
centerChanged = true;
}
}
if (!this.mapHolder.inited) {
const languageInfo = this.$locale.getCurrentLanguageInfo();
initMapInstance(this.mapHolder, this.$refs.mapContainer, {
language: languageInfo ? languageInfo.code : null,
initCenter: this.initCenter,
zoomLevel: this.zoomLevel,
text: {
zoomIn: this.$t('Zoom in'),
zoomOut: this.$t('Zoom out'),
}
});
if (this.mapHolder.inited) {
isFirstInit = true;
}
}
if (isFirstInit || centerChanged) {
setMapCenterTo(this.mapHolder, this.initCenter, this.zoomLevel);
}
if (centerChanged && this.zoomLevel > this.mapHolder.minZoomLevel) {
setMapCenterMarker(this.mapHolder, this.initCenter);
} else if (centerChanged && this.zoomLevel <= this.mapHolder.minZoomLevel) {
removeMapCenterMarker(this.mapHolder);
}
}
}
}
</script>

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