Compare commits

..

293 Commits

Author SHA1 Message Date
MaysWind d3762e6c46 add storage directory in built package 2024-08-10 19:08:56 +08:00
MaysWind 157eb140eb hide multiple accounts / categories item when there are no accounts / categories 2024-08-10 00:40:03 +08:00
MaysWind 83a0c27259 hide secret in boot log 2024-08-10 00:10:15 +08:00
MaysWind 5c4a8e37c4 verify whether required items is valid before submitting new transaction 2024-08-09 00:18:52 +08:00
MaysWind e4faf64ea3 redesign the default request id generator, replace random number to client port 2024-08-09 00:07:32 +08:00
MaysWind a4849fa4f0 add notification when user registers 2024-08-08 22:29:01 +08:00
MaysWind 32155ca63d code refactor 2024-08-08 22:26:57 +08:00
MaysWind 9ea3327517 don't create log file if request log or query log are not enabled 2024-08-08 22:18:56 +08:00
MaysWind 946a7810a7 support logging request logs and database query logs to separate files, and support rotating log files 2024-08-08 02:03:57 +08:00
MaysWind ea8021b359 modify log format 2024-08-07 22:47:56 +08:00
MaysWind caa27841ef fix the incorrect calculation of monthly income and expense amount when filtering multiple accounts 2024-08-07 00:54:57 +08:00
MaysWind d1cd13723a add online demo url 2024-08-06 01:11:57 +08:00
MaysWind 0e946a4b3b show notification in frontend 2024-08-06 00:40:27 +08:00
MaysWind 051c319890 use client language if user language is set to system default 2024-08-05 23:58:52 +08:00
MaysWind f2baa4ae65 code refactor 2024-08-05 01:29:53 +08:00
MaysWind 05a93667eb display notification every time users open the app or login 2024-08-05 01:25:26 +08:00
MaysWind c137156c97 code refactor 2024-08-05 01:15:33 +08:00
MaysWind 8f8a94cd66 add comment 2024-08-05 00:41:46 +08:00
MaysWind 4cdb599bf3 add tips 2024-08-05 00:22:58 +08:00
MaysWind 77a5ccd796 support editing templates for mobile version 2024-08-04 23:28:15 +08:00
MaysWind 99ae18d06d code refactor 2024-08-04 19:51:21 +08:00
MaysWind 90318d5690 fix adding transaction bug on desktop version 2024-08-04 19:50:33 +08:00
MaysWind f133692002 check whether input is valid before submitting 2024-08-04 19:50:26 +08:00
MaysWind 0b17251f94 modify style 2024-08-04 19:50:17 +08:00
MaysWind 24724bb19f show transaction template count in data management page 2024-08-04 19:50:04 +08:00
MaysWind b23d630daa code refactor 2024-08-04 16:21:13 +08:00
MaysWind fcb954ff40 support creating transaction from template 2024-08-04 01:07:50 +08:00
MaysWind 889225301c add translating guide link 2024-08-03 21:15:33 +08:00
MaysWind 816d0e7ceb code refactor 2024-08-03 19:24:59 +08:00
MaysWind f2c0ffab99 code refactor 2024-08-03 19:11:30 +08:00
MaysWind f1f61a9038 code refactor 2024-08-03 18:21:04 +08:00
MaysWind 77439f675b code refactor 2024-08-03 17:04:29 +08:00
MaysWind c57f17233a support currency unit name 2024-08-03 16:54:54 +08:00
MaysWind c91a56547f update currency symbols and currency symbol supports plural symbol 2024-08-03 16:06:59 +08:00
MaysWind 10dc2d1713 update currency names 2024-08-03 15:38:06 +08:00
MaysWind d3c8a520ca update latest currencies 2024-08-02 00:57:23 +08:00
MaysWind 681f888529 fix the amount in transaction edit dialog may not display after unhiding the amount 2024-08-01 00:54:55 +08:00
MaysWind bce8c23b05 fix the incorrect range of editable transaction range 2024-08-01 00:45:57 +08:00
MaysWind 788bfa7d4b fix the default user avatar not display when user cannot load user avatar url 2024-08-01 00:39:00 +08:00
MaysWind 6d331c873b check whether account category is valid when creating account 2024-08-01 00:17:17 +08:00
MaysWind a03df7ed36 create transaction template and modify template name in edit dialog 2024-08-01 00:04:07 +08:00
MaysWind b0b330903c transaction template supports setting whether hide amount 2024-07-30 00:08:48 +08:00
MaysWind 4e16f963a8 modify style 2024-07-29 23:56:33 +08:00
MaysWind 8ef4b5537c display a tooltip icon when hover the avatar 2024-07-29 23:54:52 +08:00
MaysWind 8273e06e43 code refactor 2024-07-29 23:08:04 +08:00
MaysWind b91305c490 modify setting key 2024-07-29 22:37:59 +08:00
MaysWind de086aa29e add transaction template 2024-07-29 01:27:11 +08:00
MaysWind 4c69243bef modify log 2024-07-28 20:16:02 +08:00
MaysWind 57d8edea0a modify index 2024-07-28 18:32:59 +08:00
MaysWind a098c100d7 code refactor 2024-07-28 18:01:27 +08:00
MaysWind 8fb209440d update comment 2024-07-28 17:36:51 +08:00
MaysWind d05736d0eb support minio as object storage 2024-07-28 17:36:42 +08:00
MaysWind c92a9e61b0 code refactor 2024-07-28 16:04:34 +08:00
MaysWind 2e04affb00 supports local file system object storage and use it as the default avatar provider 2024-07-28 16:03:20 +08:00
MaysWind 731b6e8bad update comment 2024-07-27 22:08:08 +08:00
MaysWind 2b63e50837 fix category name not show in transaction edit dialog when category is hidden 2024-07-26 00:50:58 +08:00
MaysWind e22f512f22 show "without tags" in table header 2024-07-26 00:29:42 +08:00
MaysWind 9a83393290 modify style 2024-07-26 00:28:08 +08:00
MaysWind 30ebe49875 allow to filter without tags 2024-07-26 00:22:11 +08:00
MaysWind e15a850cfe allow to filter with all tags 2024-07-26 00:07:59 +08:00
MaysWind 34ebc06a8d aadd transaction tag filter to backend 2024-07-26 00:00:22 +08:00
MaysWind a86428cc4d remove unused code 2024-07-25 22:39:50 +08:00
MaysWind efce4cc04e remove clear button 2024-07-25 01:07:04 +08:00
MaysWind a8484cfcaf remove display password button 2024-07-25 01:03:25 +08:00
MaysWind cc55b98e80 PIN code input supports Home & End keys and does not process f1-f12 keys and alt key 2024-07-25 00:51:19 +08:00
MaysWind 0620194c78 support showing hidden categories in filtering page / dialog 2024-07-25 00:43:21 +08:00
MaysWind a00a67f3d1 set select button disabled when there are no visible items 2024-07-25 00:18:26 +08:00
MaysWind 6b9ad1a1c8 set display transaction tags in transaction list page by default 2024-07-24 23:52:15 +08:00
MaysWind 3d0b993c45 modify style 2024-07-24 23:49:43 +08:00
MaysWind dc74ac0d0b code refactor 2024-07-24 23:48:19 +08:00
MaysWind 7ed923b347 hide sub accounts whose parent account is hidden 2024-07-24 23:36:56 +08:00
MaysWind 1f63aa8cdf show no available accounts when all accounts are hidden 2024-07-24 23:30:53 +08:00
MaysWind 84f7eab95d show no available tags when all tags are hidden 2024-07-24 23:30:33 +08:00
MaysWind 7d6c7f49e5 support showing hidden accounts in filtering page / dialog 2024-07-24 01:16:07 +08:00
MaysWind 021e523d63 support showing hidden tags in filtering page / dialog 2024-07-24 01:16:00 +08:00
MaysWind 266dafa4a9 add comment 2024-07-23 23:50:04 +08:00
MaysWind 579c903398 not allow to add / modify / delete transaction with account whose parent account is hidden 2024-07-23 23:49:53 +08:00
MaysWind 5d2e880bc5 not allow to add / modify / delete transaction with parent account 2024-07-23 23:18:34 +08:00
MaysWind 8085f7cf11 modify style 2024-07-23 01:34:35 +08:00
MaysWind aea4cf7e8b fix the bug that hidden tags don't display in transaction detail page 2024-07-23 01:12:31 +08:00
MaysWind 3d56bfa114 not allow to add transaction with hidden transaction tag 2024-07-23 00:53:31 +08:00
MaysWind a7280bf7ed not allow to add transaction with hidden transaction category 2024-07-23 00:47:19 +08:00
MaysWind 085f9817fc not allow to set hidden account as default account 2024-07-23 00:28:20 +08:00
MaysWind fcca77bca5 auto choose the first non-hidden category when opening transaction create page / dialog 2024-07-23 00:18:44 +08:00
MaysWind 4bf2e94a9d hide categories which are hidden in transaction edit page / dialog 2024-07-23 00:18:28 +08:00
MaysWind 95c2494545 code refactor 2024-07-23 00:18:18 +08:00
MaysWind 7662e0eb02 list sheet, tree view sheet and two column select components support hidden field 2024-07-23 00:17:45 +08:00
MaysWind 9f438dd648 fix the bug that the hidden accounts, categories or tags are not displayed in the filter menu when they are chosen in transaction list page 2024-07-22 00:50:36 +08:00
MaysWind 08a1f3a5f7 code refactor 2024-07-22 00:35:36 +08:00
MaysWind 26d77de67a add transaction tag filter to frontend 2024-07-22 00:34:37 +08:00
MaysWind 4f9ab9db75 code refactor 2024-07-21 23:35:17 +08:00
MaysWind 0b83518921 update comments 2024-07-21 20:56:46 +08:00
MaysWind 0f8de8d699 custom map tile server supports annotation layer 2024-07-21 20:50:49 +08:00
MaysWind aae23c285e map provider supports TianDiTu 2024-07-21 20:00:57 +08:00
MaysWind daf73dc964 code refactor 2024-07-21 18:59:00 +08:00
MaysWind bc0893b518 add comment 2024-07-21 18:37:32 +08:00
MaysWind a87bda09f7 modify the calculation strategy of month total amount 2024-07-21 17:45:42 +08:00
MaysWind aa7652279e remove unused parameter 2024-07-21 17:04:47 +08:00
MaysWind d86dea4081 modify hint 2024-07-16 00:39:49 +08:00
MaysWind 15d476fd40 modify log content prefix 2024-07-16 00:32:32 +08:00
MaysWind 453a9ff61c code refactor 2024-07-16 00:18:43 +08:00
MaysWind 26e9a0ef2a only show chart data type of categorical analysis in statistics setting page 2024-07-15 01:25:33 +08:00
MaysWind 7849b2f05c update url address when changing the settings on the statistics analysis page 2024-07-15 01:13:45 +08:00
MaysWind 2cbcc40ca9 fix the bug that the overview amounts in home page would not update after changing the first day of week 2024-07-14 18:15:41 +08:00
MaysWind 184dad8185 code refactor 2024-07-14 18:14:08 +08:00
MaysWind 46a7cd441f code refactor 2024-07-14 18:07:13 +08:00
MaysWind 55249e07a3 support setting token min refresh interval 2024-07-14 17:47:35 +08:00
MaysWind d4850b4a18 fix the bug that the first day of week setting does not take effect in the transaction list page in desktop version 2024-07-14 16:59:07 +08:00
MaysWind 93819d5894 check whether the setting value is valid and modify the allowed minimum value of settings 2024-07-14 16:15:11 +08:00
MaysWind 4a4cec3d69 modify default token expired time in code 2024-07-14 15:32:28 +08:00
MaysWind 432993121c fix the bug that cannot use multiple sessions to access at the same time after the application lock is enabled 2024-07-14 14:26:08 +08:00
MaysWind 1ce0c62c30 update local expense / income amount color settings 2024-07-14 11:09:09 +08:00
MaysWind b1343ba92a support setting expense / income amount color 2024-07-14 01:00:14 +08:00
MaysWind 84a96d80b7 support showing transaction tag in transaction list page 2024-07-10 01:08:21 +08:00
MaysWind 4b249a0ebb code refactor 2024-07-09 22:40:53 +08:00
MaysWind 58de308f30 modify variable name 2024-07-09 22:23:26 +08:00
MaysWind f151eb6197 add command transaction-tag-index-fix-transaction-time to fix transaction tag index which does not have transaction time 2024-07-09 00:53:22 +08:00
MaysWind 9eaac329b9 modify log 2024-07-09 00:25:35 +08:00
MaysWind a33123022f command transaction-check supports checking whether transaction tag index has transaction time 2024-07-09 00:25:13 +08:00
MaysWind 3eac9af403 code refactor 2024-07-09 00:06:18 +08:00
MaysWind a371058096 fix the bug that the transaction time was not correctly saved into the transaction tag index table 2024-07-08 23:54:23 +08:00
MaysWind ee003160e5 update comment 2024-07-08 01:16:24 +08:00
MaysWind 847349dcbd support using duplicate checker to prevent duplicate submissions for new transaction record 2024-07-08 01:10:04 +08:00
MaysWind a2d6aff28b show warning log when secret_key is not set 2024-07-07 21:27:41 +08:00
MaysWind cc3e1f2978 code refactor 2024-07-07 17:28:25 +08:00
MaysWind dc9bf1a1d8 modify date format 2024-07-07 17:22:44 +08:00
MaysWind 32af32d02e code refactor 2024-07-07 17:07:13 +08:00
MaysWind d91c99c177 upgrade third party dependencies 2024-07-07 17:07:08 +08:00
MaysWind d0e8419b2e use the account / transaction category filter of the statistics page when navigating from the statistics page to the transaction list page 2024-07-07 14:37:20 +08:00
MaysWind dad3f1041e fix the bug that the filter does not take effect when navigating from the account or statistics page to the transaction list page 2024-07-07 14:08:53 +08:00
MaysWind 7b70b2db29 only show categories with specified type in category filter dialog / page 2024-07-07 13:37:56 +08:00
MaysWind e5a04596e1 modify style 2024-07-07 11:35:31 +08:00
MaysWind 297f8b9987 do not reload transaction list when filter is not changed actually 2024-07-07 11:18:43 +08:00
MaysWind 9eddab3dd8 modify style 2024-07-07 10:38:47 +08:00
MaysWind ec97d2df91 remove unused code 2024-07-07 10:18:53 +08:00
MaysWind 3dd39defc1 support filter multiple accounts and categories in transaction list page 2024-07-07 01:33:22 +08:00
MaysWind c0cc9b5247 code refactor 2024-07-06 21:10:52 +08:00
MaysWind dc13afc071 code refactor 2024-07-06 21:02:49 +08:00
MaysWind 628bd48f73 code refactor 2024-07-05 01:25:51 +08:00
MaysWind c827675e14 code refactor 2024-07-04 23:48:41 +08:00
MaysWind 09fb474921 code refactor 2024-07-04 23:41:02 +08:00
MaysWind 7e1338e081 fix the bug that there are duplicate transaction in transaction list response when filtering multiple accounts 2024-07-04 00:55:41 +08:00
MaysWind 7a5d7337cd code refactor 2024-07-04 00:15:32 +08:00
MaysWind b80041433c transaction list api supports filtering by multiple account / category 2024-07-03 00:21:56 +08:00
MaysWind dd32ab83cb modify style 2024-07-02 22:34:48 +08:00
MaysWind c8db448dfc fix the bug that the page cannot be loaded when clicking the date which is already chosen 2024-07-02 01:20:38 +08:00
MaysWind 51edca66d1 add more currency display type 2024-07-02 01:10:34 +08:00
MaysWind bb5529098f add comment 2024-07-02 01:07:14 +08:00
MaysWind d5a54dd1fb code refactor 2024-07-02 01:03:40 +08:00
MaysWind 329119fc3b modify text 2024-07-02 00:46:22 +08:00
MaysWind e43cf26bb5 code refactor 2024-07-01 23:27:34 +08:00
MaysWind 93bc3bf94b code refactor 2024-07-01 23:03:55 +08:00
MaysWind 675b5f039a remove unused code 2024-07-01 22:57:07 +08:00
MaysWind b3e4e39b49 code refactor 2024-07-01 01:00:55 +08:00
MaysWind 4dc072f56d code refactor 2024-07-01 00:53:29 +08:00
MaysWind b5d72c89f2 support filtering transaction amount 2024-07-01 00:48:00 +08:00
MaysWind d2b3900ed4 code refactor 2024-06-30 21:18:21 +08:00
MaysWind 4e44553c07 modify style 2024-06-30 18:13:36 +08:00
MaysWind ec6b5fb155 move type filter to more filter popover menu 2024-06-30 17:51:00 +08:00
MaysWind 16e53feaf4 fix the bug that cannot view the location on the map of an existing transaction in desktop page 2024-06-30 16:43:58 +08:00
MaysWind 182bdb34cd code refactor 2024-06-30 16:25:39 +08:00
MaysWind 59d03b54d7 move currency display type to user settings 2024-06-30 16:25:32 +08:00
MaysWind 445969c449 remove unused code 2024-06-30 12:05:41 +08:00
MaysWind 29214a3bf3 remove unused code 2024-06-30 01:52:52 +08:00
MaysWind d979dc3535 code refactor 2024-06-30 01:52:20 +08:00
MaysWind 399413a270 support setting decimal separator and digit grouping symbol 2024-06-30 01:48:36 +08:00
MaysWind d9c8142c51 code refactor 2024-06-29 17:12:02 +08:00
MaysWind c951285049 modify http response code 2024-06-29 16:15:14 +08:00
MaysWind 0e372969b3 fix wrong text 2024-06-29 15:17:36 +08:00
MaysWind 2d51f7b2be show provider of exchange rates data and map in about page 2024-06-29 14:09:47 +08:00
MaysWind 02a5dcf9ba fix the bug that the transaction view dialog does not show no location 2024-06-29 11:51:39 +08:00
MaysWind 9a70cfe24c modify style 2024-06-29 11:46:12 +08:00
MaysWind 023d875a00 modify style 2024-06-29 11:13:25 +08:00
MaysWind 640f74c612 add keyword parameters to the URL when searching for transaction descriptions in the desktop transaction list 2024-06-29 10:43:20 +08:00
MaysWind 768b005200 only show the types of categorical analysis 2024-06-28 22:24:08 +08:00
MaysWind fb315127f9 fix the bug that the frontend would not display any secondary transaction categories after modifying the primary transaction category 2024-06-24 00:51:57 +08:00
MaysWind 756e6cac5a update currency name 2024-06-24 00:42:10 +08:00
MaysWind daae5b68cd hide trend analysis settings in statistics settings page 2024-06-24 00:23:54 +08:00
MaysWind 0e391bee50 support changing primary category for transaction category 2024-06-24 00:21:47 +08:00
MaysWind 9627e65d6d display items in specified sorting type in tooltip for trend analysis chart 2024-06-23 22:29:47 +08:00
MaysWind 830974500b modify style 2024-06-23 21:56:54 +08:00
MaysWind 0438964e17 add seconds to time column in exported data 2024-06-17 00:30:15 +08:00
MaysWind 226d44651f code refactor 2024-06-17 00:22:32 +08:00
MaysWind db71ac5279 remove unused code 2024-06-17 00:20:04 +08:00
MaysWind 7e2a0b1483 set default trends date range type to this year 2024-06-10 23:48:56 +08:00
MaysWind a219444953 trends analysis supports total expense / total income / total balance 2024-06-10 23:42:41 +08:00
MaysWind 5fec41055e keep the state of selected legend 2024-06-10 23:26:04 +08:00
MaysWind 5107e451d8 hide trends chart when no transaction data 2024-06-10 22:35:45 +08:00
MaysWind 3b34cdbda2 remove unused code 2024-06-10 22:26:57 +08:00
MaysWind 35fcd32a96 show total amount on tooltip for trend chart 2024-06-10 22:19:31 +08:00
MaysWind cf325b4267 auto change y-axis width based on label width 2024-06-10 22:02:30 +08:00
MaysWind 860ef610b7 modify style 2024-06-10 22:00:55 +08:00
MaysWind 5c5e6a60a7 update README.md 2024-06-09 23:13:26 +08:00
MaysWind 435c38fb27 fix the bug that the build.bat scripts would not immediately stop when a part of the build fails 2024-06-09 23:13:16 +08:00
MaysWind d640b7a5f5 modify style 2024-06-09 23:13:07 +08:00
MaysWind ae25b4eb4e modify style 2024-06-09 23:12:52 +08:00
MaysWind 315a686fed upgrade third party dependencies 2024-06-09 23:12:36 +08:00
MaysWind 4dd79b07d9 fix the bug that cannot set the date range automatically when navigate to transaction list page by clicking category in statistics page in mobile version 2024-06-09 02:35:59 +08:00
MaysWind e88d803232 add trends analysis chart 2024-06-09 02:31:13 +08:00
MaysWind 1489854444 Fix the bug that no transaction would display when all date range in categorical analysis is selected 2024-06-09 01:30:41 +08:00
MaysWind c5f9e276b4 modify style 2024-06-09 00:57:02 +08:00
MaysWind b2a8b359d1 code refactor 2024-06-08 23:05:55 +08:00
MaysWind 72f6a9e9a3 use the preset name if custom date range matches a preset item 2024-06-03 00:47:39 +08:00
MaysWind 809172bf34 add date filter for trend analysis 2024-06-03 00:30:25 +08:00
MaysWind c34887240e trend analysis supports data from all dates 2024-06-02 22:06:52 +08:00
MaysWind f041e7cb7d add chart date type settings for trend analysis 2024-05-27 01:36:46 +08:00
MaysWind 5eca777891 add chart type and chart data type settings for trend analysis 2024-05-26 23:58:26 +08:00
MaysWind a9e3b79eb1 fix the bug that the selected date range in date selection dialog would not change to current set range after opening the date selection dialog in desktop version 2024-05-26 22:57:57 +08:00
MaysWind 0a376217c6 upgrade third party dependencies 2024-05-26 22:00:01 +08:00
MaysWind 687d062bfc modify style 2024-05-26 20:25:18 +08:00
MaysWind e123498895 code refactor 2024-05-26 19:49:04 +08:00
MaysWind a917d16c26 code refactor 2024-05-26 19:48:40 +08:00
MaysWind 0884af038d add trend analysis api 2024-05-20 00:01:40 +08:00
MaysWind 72619f3dad upgrade golang to 1.21.9, node.js to 18.20.2 2024-05-19 17:02:51 +08:00
MaysWind cf120dbcbf fix the bug that cannot load more transaction after opening and clicking cancel button custom time range dialog 2024-05-19 16:57:23 +08:00
MaysWind 9906f1b1a7 fix the bug that the "Date" is highlighted after custom date sheet opened even the date range is not set in mobile transaction list page 2024-05-01 16:40:29 +08:00
MaysWind ea32bfa5fc support setting timezone type for the time range of statistical data 2024-04-13 23:31:24 +08:00
MaysWind 14f6de8af1 remove unused code 2024-04-06 23:58:22 +08:00
MaysWind 176d5a15c5 show "-dev" after the version number when the build version is not a release version 2024-04-06 17:05:04 +08:00
MaysWind cfc7a5bd49 modify style 2024-04-06 16:37:48 +08:00
MaysWind 4b68ccf678 update text content 2024-04-06 16:33:08 +08:00
MaysWind cb57b216d0 fix the bug that the sheet would display blank after clicking now button and switching from date mode to time mode in date time selection sheet 2024-04-05 23:45:33 +08:00
MaysWind 222951139c swap the location of now and switch mode button 2024-04-05 23:33:52 +08:00
MaysWind 821c7bfbd3 modify style 2024-04-05 23:24:06 +08:00
MaysWind 722c1d7917 fix missing go.sum entry 2024-04-05 23:06:42 +08:00
MaysWind 4d065bc724 fix the bug that the status of email changes to unverified after clicking reset button in the user basic setting page 2024-04-05 22:57:48 +08:00
MaysWind 2a2cb3acc9 modify style 2024-04-05 22:45:06 +08:00
MaysWind 4a16b82700 upgrade Materio to 2.2.1 2024-04-05 00:19:48 +08:00
MaysWind ea97f8cf7a update third party dependencies 2024-04-04 21:16:40 +08:00
MaysWind 28f113d992 code refactor 2024-04-04 20:44:37 +08:00
MaysWind 46caf46ef7 sort languages by language code 2024-03-30 15:41:34 +08:00
MaysWind 185758b638 use current platform to build frontend assets 2024-03-25 00:30:56 +08:00
MaysWind 1e047aed80 code refactor 2024-03-24 17:41:17 +08:00
MaysWind 8ef7676b4f modify text 2024-03-24 15:41:52 +08:00
MaysWind b2fc24d5ae add geographic location to exported file 2024-03-24 15:38:09 +08:00
MaysWind 065cd9dff8 modify text 2024-03-24 15:14:13 +08:00
MaysWind 532c762553 code refactor 2024-03-24 13:54:13 +08:00
MaysWind 4857055eaf modify text 2024-03-24 13:44:57 +08:00
MaysWind 604379bf85 modify style 2024-03-24 13:35:29 +08:00
MaysWind 3d0f793cf6 code refactor 2024-03-24 01:41:42 +08:00
MaysWind 9f8634ac11 add date navigation button in desktop version transaction list page 2024-03-24 01:40:55 +08:00
MaysWind 6ab70b97b4 move all date to the top of navigation bar 2024-03-24 01:16:07 +08:00
MaysWind 8c164ec833 modify style 2024-03-24 01:09:42 +08:00
MaysWind 09d47459b6 add toggle calendar and time picker button 2024-03-10 20:48:20 +08:00
MaysWind 2916106f27 auto set destination amount by source amount when destination amount is zero 2024-03-10 19:55:29 +08:00
MaysWind 6b4292596a add date navigation button in mobile version transaction list page 2024-03-10 19:05:55 +08:00
MaysWind 78aebb7d6c show the transaction time in default timezone on tooltip in desktop version transaction list page when the transaction time zone is not the default time zone 2024-03-10 13:45:21 +08:00
MaysWind 771bb35e9f show the transaction time in default timezone by clicking the transaction time in mobile version transaction view page when the transaction time zone is not the default time zone 2024-03-10 13:45:08 +08:00
MaysWind 453ed4227d modify style 2024-03-10 13:44:41 +08:00
MaysWind 88a284a21b use default cursor when input is readonly 2024-03-10 11:52:50 +08:00
MaysWind 9488b85705 fix the bug that the time picker for mobile device displays and sets incorrectly when the default time zone is not the browser time zone 2024-03-10 02:34:28 +08:00
MaysWind 8be3fd7ed4 remove unused import 2024-03-10 01:15:10 +08:00
MaysWind 8fdc0019ea fix the bug that the text size of action menu title label does not follow the app text size change 2024-03-09 23:02:25 +08:00
MaysWind b21dd73ff2 fix default time is wrong in add transaction dialog when default time zone is not the browser time zone in desktop version 2024-03-09 22:49:07 +08:00
MaysWind 24432701dd modify style 2024-03-09 22:17:28 +08:00
MaysWind 802cdabd75 the order of year and month in the date picker is based on the order in long date format set by the user 2024-03-09 21:51:23 +08:00
MaysWind 8c83af1543 add swap account and/or amount menu in transaction edit page 2024-03-04 00:28:20 +08:00
MaysWind deb465377e show exchange rate in transaction edit page when the currencies of source account and destination account are different 2024-03-04 00:09:21 +08:00
MaysWind bed2aef7f8 use time picker in datetime selection sheet for mobile device 2024-03-03 23:01:27 +08:00
MaysWind 57b2b2492f fix cannot build frontend on arm/v6 or arm/v7 platform 2024-03-03 18:52:14 +08:00
MaysWind 87cdaee547 update base image and go/node version 2024-03-03 16:58:05 +08:00
MaysWind a8b0ef2483 update third party dependencies 2024-03-03 14:06:36 +08:00
MaysWind 63e976e5cb update third party dependencies 2024-03-03 13:33:28 +08:00
MaysWind f73830d37b bump year to 2024 2024-03-03 13:19:48 +08:00
MaysWind 6511c4e810 update exchange rates data api of National Bank Of Poland 2024-03-03 12:12:11 +08:00
MaysWind 008c58f52b use custom user-agent to request exchange rates data 2024-03-03 11:51:58 +08:00
MaysWind fa4a17f47b support setting proxy to request exchange rates or map data 2024-03-03 11:46:30 +08:00
MaysWind 3fc2a763b4 set navigation button disabled when reloading 2023-12-04 00:23:53 +08:00
MaysWind 1b2726a55c update third party dependencies 2023-12-03 20:47:59 +08:00
MaysWind 229fa27b73 modify style 2023-11-19 22:12:09 +08:00
f97 072f44245d typo: third-party-dependencies.json 2023-11-01 21:55:54 +08:00
MaysWind dc837c430f support export to tsv file 2023-10-29 21:05:17 +08:00
MaysWind 429e270a9e modify x-small chip style 2023-10-29 16:33:55 +08:00
MaysWind 985cefc297 update third party dependencies 2023-10-29 16:28:36 +08:00
MaysWind 285fc0ced3 map provider supports CartoDB 2023-10-29 16:18:52 +08:00
MaysWind 777e14b6d4 show error when request file name extension is invalid when using map image proxy 2023-10-29 14:54:58 +08:00
MaysWind d18e8211ca show error when specified map provider is not current provider when using map image proxy 2023-10-29 14:47:18 +08:00
MaysWind 2984980a54 code refactor 2023-10-29 14:39:44 +08:00
MaysWind 2302f31f18 set min zoom level to leaflet control 2023-10-29 14:16:47 +08:00
MaysWind f673677c2a support custom map tile server url 2023-10-29 14:09:51 +08:00
MaysWind acc9bf77fb update third party dependencies 2023-10-16 00:29:51 +08:00
MaysWind 678769da0e code refactor 2023-10-16 00:20:00 +08:00
MaysWind 2389208c95 optimize service worker config 2023-10-09 21:48:41 +08:00
MaysWind 28a1080ccc fix redundant code 2023-09-26 01:00:54 +08:00
MaysWind 432d3fd3c3 add missing files when building package 2023-09-26 00:57:56 +08:00
MaysWind 8fc73b866b fix the issue of checking the result code 2023-09-26 00:52:39 +08:00
MaysWind dbc62aac93 fix unit test failure 2023-09-25 23:37:16 +08:00
MaysWind 79802a46fd add build script for windows 2023-09-25 01:28:38 +08:00
MaysWind 3d8ecb42c1 change disabled column nullable 2023-09-24 22:34:00 +08:00
MaysWind bcaf554ae4 navigate to the home page immediately after sign up successfully 2023-09-18 01:41:35 +08:00
MaysWind 3ac4a921e0 update vue-datepicker to 6.1.0 2023-09-18 01:27:54 +08:00
MaysWind 128f86b643 remove redundant code 2023-09-18 01:27:15 +08:00
MaysWind 828fc690b6 bump version to 0.5.0 2023-09-18 01:11:04 +08:00
295 changed files with 27204 additions and 11411 deletions
+1
View File
@@ -31,6 +31,7 @@ jobs:
- name: Set up the environment
run: |
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: Set up the environment
run: |
sed -i 's#FROM #FROM ${{ secrets.DOCKER_REPO }}/mirrors/#g' Dockerfile
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
+5 -4
View File
@@ -1,5 +1,5 @@
# Build backend binary file
FROM golang:1.20.8-alpine3.17 AS be-builder
FROM golang:1.21.12-alpine3.20 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.17.1-alpine3.17 AS fe-builder
FROM --platform=$BUILDPLATFORM node:18.20.3-alpine3.20 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.5
FROM alpine:3.20.1
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
@@ -27,7 +27,8 @@ COPY docker/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
RUN mkdir -p /ezbookkeeping && chown 1000:1000 /ezbookkeeping \
&& mkdir -p /ezbookkeeping/data && chown 1000:1000 /ezbookkeeping/data \
&& mkdir -p /ezbookkeeping/log && chown 1000:1000 /ezbookkeeping/log
&& mkdir -p /ezbookkeeping/log && chown 1000:1000 /ezbookkeeping/log \
&& mkdir -p /ezbookkeeping/storage && chown 1000:1000 /ezbookkeeping/storage
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
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2023 MaysWind (i@mayswind.net)
Copyright (c) 2020-2024 MaysWind (i@mayswind.net)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+19 -1
View File
@@ -8,6 +8,8 @@
## Introduction
ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It can be deployed on almost all platforms, including Windows, macOS and Linux on x86, amd64 and ARM architectures. You can even deploy it on an raspberry device. It also supports many different databases, including sqlite and mysql. With docker, you can just deploy it via one command without complicated configuration.
Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.mayswind.net)
## Features
1. Open source & Self-hosted
2. Lightweight & Fast
@@ -53,19 +55,35 @@ Latest Daily Build:
### Install from binary
Latest release: [https://github.com/mayswind/ezbookkeeping/releases](https://github.com/mayswind/ezbookkeeping/releases)
**Linux / macOS**
$ ./ezbookkeeping server run
ezBookkeeping will listen at port 8080 as default. Then you can visit http://{YOUR_HOST_ADDRESS}:8080/ .
**Windows**
> .\ezbookkeeping.exe server run
ezBookkeeping will listen at port 8080 as default. Then you can visit `http://{YOUR_HOST_ADDRESS}:8080/` .
### Build from source
Make sure you have [Golang](https://golang.org/), [GCC](http://gcc.gnu.org/), [Node.js](https://nodejs.org/) and [NPM](https://www.npmjs.com/) installed. Then download the source code, and follow these steps:
**Linux / macOS**
$ ./build.sh package -o ezbookkeeping.tar.gz
All the files will be packaged in `ezbookkeeping.tar.gz`.
**Windows**
> .\build.bat package -o ezbookkeeping.zip
All the files will be packaged in `ezbookkeeping.zip`.
You can also build docker image, make sure you have [docker](https://www.docker.com/) installed, then follow these steps:
**Linux**
$ ./build.sh docker
## Documents
+264
View File
@@ -0,0 +1,264 @@
@echo off
set "TYPE="
set "NO_LINT=0"
set "NO_TEST=0"
set "RELEASE=%RELEASE_BUILD%"
set "RELEASE_TYPE=unknown"
set "VERSION="
set "COMMIT_HASH="
set "BUILD_UNIXTIME="
set "BUILD_DATE="
set "PACKAGE_FILENAME="
for /f %%a in ('"prompt $E$S & echo on & for %%b in (1) do rem"') do set "ESC=%%a"
if "%~1"=="" call :show_help & goto :end
goto :pre_parse_args
:echo_red
echo %ESC%[91m%~1%ESC%[0m
goto :eof
:set_unixtime
setlocal enableextensions
for /f %%x in ('wmic path win32_utctime get /format:list ^| findstr "="') do set %%x
set /a z=(14-100%Month%%%100)/12, y=10000%Year%%%10000-z
set /a ut=y*365+y/4-y/100+y/400+(153*(100%Month%%%100+12*z-3)+2)/5+Day-719469
set /a ut=ut*86400+100%Hour%%%100*3600+100%Minute%%%100*60+100%Second%%%100
endlocal & set "%1=%ut%" & goto :eof
:set_date
setlocal enableextensions
for /f %%x in ('wmic path win32_localtime get /format:list ^| findstr "="') do set %%x
if %Month% lss 10 set "Month=0%Month%"
if %Day% lss 10 set "Day=0%Day%"
endlocal & set "%1=%Year%%Month%%Day%" & goto :eof
:check_dependency
if "%~1"=="" goto :eof
where /q %~1 || call :echo_red "Error: "%~1" is required." && goto :end
shift
goto :check_dependency
:show_help
echo ezBookkeeping build script for Windows
echo.
echo Usage:
echo build.cmd type [options]
echo.
echo Types:
echo backend Build backend binary file
echo frontend Build frontend files
echo package Build package archive
echo.
echo Options:
echo /r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
echo /o, --output ^<filename^> Package file name (For "package" type only)
echo --no-lint Do not execute lint check before building
echo --no-test Do not execute unit testing before building
echo /h, --help Show help
goto :eof
:pre_parse_args
if "%~1"=="" goto :post_parse_args
if /i "%~1"=="backend" set "TYPE=%~1" & shift
if /i "%~1"=="frontend" set "TYPE=%~1" & shift
if /i "%~1"=="package" set "TYPE=%~1" & shift
:parse_args
if "%~1"=="" goto :post_parse_args
if /i "%~1"=="/r" set "RELEASE=1" & shift & goto :parse_args
if /i "%~1"=="-r" set "RELEASE=1" & shift & goto :parse_args
if /i "%~1"=="--release" set "RELEASE=1" & shift & goto :parse_args
if /i "%~1"=="/o" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
if /i "%~1"=="-o" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
if /i "%~1"=="--output" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
if /i "%~1"=="--no-lint" set "NO_LINT=1" & shift & goto :parse_args
if /i "%~1"=="--no-test" set "NO_TEST=1" & shift & goto :parse_args
if /i "%~1"=="/h" call :show_help & goto :end
if /i "%~1"=="-h" call :show_help & goto :end
if /i "%~1"=="--help" call :show_help & goto :end
call :echo_red "Invalid argument: %~1" & call :show_help & goto :end
:post_parse_args
if "%RELEASE%"=="" set "RELEASE=0"
if "%RELEASE%"=="0" (
set "RELEASE_TYPE=snapshot"
) else (
set "RELEASE_TYPE=release"
)
:check_type_dependencies
if not defined TYPE call :echo_red "Error: No specified type" & call :show_help & goto :end
call :check_dependency "git"
if "%TYPE%"=="backend" call :check_dependency "go" "gcc"
if "%TYPE%"=="frontend" call :check_dependency "node" "npm"
if "%TYPE%"=="package" call :check_dependency "go" "gcc" "node" "npm" "7z"
if not "%errorlevel%"=="0" goto :end
:set_build_parameters
for /f "tokens=2 delims=:" %%x in ('findstr "\"version\": \"*\"," package.json') do set "VERSION=%%x"
set VERSION=%VERSION: =%
set VERSION=%VERSION:,=%
set VERSION=%VERSION:"=%
for /f %%x in ('git rev-parse --short HEAD') do set "COMMIT_HASH=%%x"
call :set_unixtime BUILD_UNIXTIME
call :set_date BUILD_DATE
:main
if "%TYPE%"=="backend" call :build_backend & goto :end
if "%TYPE%"=="frontend" call :build_frontend & goto :end
if "%TYPE%"=="package" call :build_package & goto :end
goto :end
:build_backend
setlocal enabledelayedexpansion
echo Pulling backend dependencies...
call go get .
if "%NO_LINT%"=="0" (
echo Executing backend lint checking...
call go vet -v .\...
if !errorlevel! neq 0 (
call :echo_red "Error: Failed to pass lint checking"
goto :end
)
)
if "%NO_TEST%"=="0" (
echo Executing backend unit testing...
call go clean -cache
call go test .\... -v
if !errorlevel! neq 0 (
call :echo_red "Error: Failed to pass unit testing"
goto :end
)
)
endlocal
set "CGO_ENABLED=1"
setlocal
set "backend_build_extra_arguments=-X main.Version=%VERSION%"
set "backend_build_extra_arguments=%backend_build_extra_arguments% -X main.CommitHash=%COMMIT_HASH%"
if "%RELEASE%"=="0" (
set "backend_build_extra_arguments=%backend_build_extra_arguments% -X main.BuildUnixTime=%BUILD_UNIXTIME%"
)
echo Building backend binary file (%RELEASE_TYPE%)...
call go build -a -v -trimpath -tags timetzdata -ldflags "-w -s -linkmode external -extldflags '-static' %backend_build_extra_arguments%" -o ezbookkeeping.exe ezbookkeeping.go
endlocal
set "CGO_ENABLED="
goto :eof
:build_frontend
setlocal enabledelayedexpansion
echo Pulling frontend dependencies...
call npm install
if "%NO_LINT%"=="0" (
echo Executing frontend lint checking...
call npm run lint
if !errorlevel! neq 0 (
call :echo_red "Error: Failed to pass lint checking"
goto :end
)
)
endlocal
echo Building frontend files(%RELEASE_TYPE%)...
if "%RELEASE%"=="0" (
set "buildUnixTime=%BUILD_UNIXTIME%"
call npm run build
set "buildUnixTime="
) else (
call npm run build
)
goto :eof
:build_package
setlocal enabledelayedexpansion
set "package_file_name=%VERSION%"
if "%RELEASE%"=="0" (
set "build_date="
set "package_file_name=%package_file_name%-%build_date%"
)
set "package_file_name=ezbookkeeping-%package_file_name%-windows.zip"
if defined PACKAGE_FILENAME set "package_file_name=%PACKAGE_FILENAME%"
echo Building package archive "%package_file_name%" (%RELEASE_TYPE%)...
call :build_backend
if !errorlevel! neq 0 (
goto :end
)
call :build_frontend
if !errorlevel! neq 0 (
goto :end
)
rmdir package /s /q
mkdir package
mkdir package\data
mkdir package\storage
mkdir package\log
xcopy ezbookkeeping.exe package\
xcopy dist package\public /e /i
xcopy conf package\conf /e /i
xcopy templates package\templates /e /i
xcopy LICENSE package\
cd package
if !errorlevel! neq 0 (
call :echo_red "Error: Build Failed"
goto :end
)
call 7z a -r -tzip -mx9 ..\%package_file_name% package *
cd ..
endlocal
goto :eof
:end
set "TYPE="
set "NO_LINT="
set "NO_TEST="
set "RELEASE="
set "RELEASE_TYPE="
set "VERSION="
set "COMMIT_HASH="
set "BUILD_UNIXTIME="
set "BUILD_DATE="
set "PACKAGE_FILENAME="
exit /B
+2
View File
@@ -202,10 +202,12 @@ build_package() {
rm -rf package
mkdir package
mkdir package/data
mkdir package/storage
mkdir package/log
cp ezbookkeeping package/
cp -R dist package/public
cp -R conf package/conf
cp -R templates package/templates
cp LICENSE package/
cd package || { echo_red "Error: Build Failed"; exit 1; }
+10 -2
View File
@@ -58,7 +58,7 @@ func updateAllDatabaseTablesStructure() error {
return err
}
log.BootInfof("[database.updateAllDatabaseTablesStructure] two factor table maintained successfully")
log.BootInfof("[database.updateAllDatabaseTablesStructure] two-factor table maintained successfully")
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactorRecoveryCode))
@@ -66,7 +66,7 @@ func updateAllDatabaseTablesStructure() error {
return err
}
log.BootInfof("[database.updateAllDatabaseTablesStructure] two factor recovery code table maintained successfully")
log.BootInfof("[database.updateAllDatabaseTablesStructure] two-factor recovery code table maintained successfully")
err = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord))
@@ -116,5 +116,13 @@ func updateAllDatabaseTablesStructure() error {
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTemplate))
if err != nil {
return err
}
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction template table maintained successfully")
return nil
}
+26
View File
@@ -7,10 +7,12 @@ import (
"github.com/urfave/cli/v2"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"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/storage"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
@@ -55,6 +57,10 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
return nil, err
}
if config.SecretKeyNoSet {
log.BootWarnf("[initializer.initializeSystem] \"secret_key\" in config file is not set, please change it to keep your user data safe")
}
settings.SetCurrentConfig(config)
err = datastore.InitializeDataStore(config)
@@ -75,6 +81,15 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
return nil, err
}
err = storage.InitializeStorageContainer(config)
if err != nil {
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes object storage failed, because %s", err.Error())
}
return nil, err
}
err = uuid.InitializeUuidGenerator(config)
if err != nil {
@@ -84,6 +99,15 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
return nil, err
}
err = duplicatechecker.InitializeDuplicateChecker(config)
if err != nil {
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes duplicate checker failed, because %s", err.Error())
}
return nil, err
}
err = mail.InitializeMailer(config)
if err != nil {
@@ -121,7 +145,9 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
clonedConfig.DatabaseConfig.DatabasePassword = "****"
clonedConfig.SMTPConfig.SMTPPasswd = "****"
clonedConfig.MinIOConfig.SecretAccessKey = "****"
clonedConfig.SecretKey = "****"
clonedConfig.AmapApplicationSecret = "****"
return clonedConfig
}
+62 -6
View File
@@ -2,13 +2,14 @@ package cmd
import (
"fmt"
"github.com/mayswind/ezbookkeeping/pkg/models"
"os"
"github.com/urfave/cli/v2"
clis "github.com/mayswind/ezbookkeeping/pkg/cli"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -229,9 +230,22 @@ var UserData = &cli.Command{
},
},
},
{
Name: "transaction-tag-index-fix-transaction-time",
Usage: "Fix the transaction tag index data which does not have transaction time",
Action: fixTransactionTagIndexNotHaveTransactionTime,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "transaction-export",
Usage: "Export user all transactions to csv file",
Usage: "Export user all transactions to file",
Action: exportUserTransaction,
Flags: []cli.Flag{
&cli.StringFlag{
@@ -246,6 +260,12 @@ var UserData = &cli.Command{
Required: true,
Usage: "Specific exported file path (e.g. transaction.csv)",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Required: false,
Usage: "Export file type, support csv or tsv, default is csv",
},
},
},
},
@@ -468,11 +488,11 @@ func disableUser2FA(c *cli.Context) error {
err = clis.UserData.DisableUserTwoFactorAuthorization(c, username)
if err != nil {
log.BootErrorf("[user_data.disableUser2FA] error occurs when disabling user two factor authorization")
log.BootErrorf("[user_data.disableUser2FA] error occurs when disabling user two-factor authorization")
return err
}
log.BootInfof("[user_data.disableUser2FA] two factor authorization of user \"%s\" has been disabled", username)
log.BootInfof("[user_data.disableUser2FA] two-factor authorization of user \"%s\" has been disabled", username)
return nil
}
@@ -546,6 +566,29 @@ func checkUserTransactionAndAccount(c *cli.Context) error {
return nil
}
func fixTransactionTagIndexNotHaveTransactionTime(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
log.BootInfof("[user_data.fixTransactionTagIndexNotHaveTransactionTime] starting fixing user \"%s\" transaction tag index data", username)
_, err = clis.UserData.FixTransactionTagIndexWithTransactionTime(c, username)
if err != nil {
log.BootErrorf("[user_data.fixTransactionTagIndexNotHaveTransactionTime] error occurs when fixing user data")
return err
}
log.BootInfof("[user_data.fixTransactionTagIndexNotHaveTransactionTime] user transaction tag index data has been fixed successfully")
return nil
}
func exportUserTransaction(c *cli.Context) error {
_, err := initializeSystem(c)
@@ -555,9 +598,15 @@ func exportUserTransaction(c *cli.Context) error {
username := c.String("username")
filePath := c.String("file")
fileType := c.String("type")
if fileType != "" && fileType != "csv" && fileType != "tsv" {
log.BootErrorf("[user_data.exportUserTransaction] export file type is not supported")
return errs.ErrNotSupported
}
if filePath == "" {
log.BootErrorf("[user_data.exportUserTransaction] export file path is not specified")
log.BootErrorf("[user_data.exportUserTransaction] export file path is unspecified")
return os.ErrNotExist
}
@@ -570,7 +619,7 @@ func exportUserTransaction(c *cli.Context) error {
log.BootInfof("[user_data.exportUserTransaction] starting exporting user \"%s\" data", username)
content, err := clis.UserData.ExportTransaction(c, username)
content, err := clis.UserData.ExportTransaction(c, username, fileType)
if err != nil {
log.BootErrorf("[user_data.exportUserTransaction] error occurs when exporting user data")
@@ -605,6 +654,12 @@ func printUserInfo(user *models.User) {
fmt.Printf("[ShortDateFormat] %s (%d)\n", user.ShortDateFormat, user.ShortDateFormat)
fmt.Printf("[LongTimeFormat] %s (%d)\n", user.LongTimeFormat, user.LongTimeFormat)
fmt.Printf("[ShortTimeFormat] %s (%d)\n", user.ShortTimeFormat, user.ShortTimeFormat)
fmt.Printf("[DecimalSeparator] %s (%d)\n", user.DecimalSeparator, user.DecimalSeparator)
fmt.Printf("[DigitGroupingSymbol] %s (%d)\n", user.DigitGroupingSymbol, user.DigitGroupingSymbol)
fmt.Printf("[DigitGrouping] %s (%d)\n", user.DigitGrouping, user.DigitGrouping)
fmt.Printf("[CurrencyDisplayType] %s (%d)\n", user.CurrencyDisplayType, user.CurrencyDisplayType)
fmt.Printf("[ExpenseAmountColor] %s (%d)\n", user.ExpenseAmountColor, user.ExpenseAmountColor)
fmt.Printf("[IncomeAmountColor] %s (%d)\n", user.IncomeAmountColor, user.IncomeAmountColor)
fmt.Printf("[Deleted] %t\n", user.Deleted)
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
@@ -625,5 +680,6 @@ func printUserInfo(user *models.User) {
func printTokenInfo(token *models.TokenRecord) {
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.CreatedUnixTime), token.CreatedUnixTime)
fmt.Printf("[ExpiredAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.ExpiredUnixTime), token.ExpiredUnixTime)
fmt.Printf("[LastSeen] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.LastSeenUnixTime), token.LastSeenUnixTime)
fmt.Printf("[UserAgent] %s\n", token.UserAgent)
}
+3 -2
View File
@@ -66,7 +66,7 @@ func parseRequestId(c *cli.Context) error {
return err
}
newRequestId := defaultGenerator.GenerateRequestId(net.IPv4zero.String())
newRequestId := defaultGenerator.GenerateRequestId(net.IPv4zero.String(), 0)
newRequestIdInfo, err := defaultGenerator.ParseRequestIdInfo(newRequestId)
printRequestIdInfo(requestId, requestIdInfo, newRequestIdInfo)
@@ -114,7 +114,6 @@ func printRequestIdInfo(requestId string, requestIdInfo *requestid.RequestIdInfo
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)
@@ -125,4 +124,6 @@ func printRequestIdInfo(requestId string, requestIdInfo *requestid.RequestIdInfo
binary.BigEndian.PutUint32(ip, requestIdInfo.ClientIp)
fmt.Printf("[ClientIpv4] %s\n", ip.String())
}
fmt.Printf("[ClientPort] %d\n", requestIdInfo.ClientPort)
}
+75 -17
View File
@@ -44,13 +44,13 @@ func startWebServer(c *cli.Context) error {
return err
}
log.BootInfof("[server.startWebServer] static root path is %s", config.StaticRootPath)
log.BootInfof("[webserver.startWebServer] static root path is %s", config.StaticRootPath)
if config.AutoUpdateDatabase {
err = updateAllDatabaseTablesStructure()
if err != nil {
log.BootErrorf("[server.startWebServer] update database table structure failed, because %s", err.Error())
log.BootErrorf("[webserver.startWebServer] update database table structure failed, because %s", err.Error())
return err
}
}
@@ -58,7 +58,7 @@ func startWebServer(c *cli.Context) error {
err = requestid.InitializeRequestIdGenerator(config)
if err != nil {
log.BootErrorf("[server.startWebServer] initializes requestid generator failed, because %s", err.Error())
log.BootErrorf("[webserver.startWebServer] initializes requestid generator failed, because %s", err.Error())
return err
}
@@ -68,7 +68,7 @@ func startWebServer(c *cli.Context) error {
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
}
log.BootInfof("[server.startWebServer] %s%s", serverInfo, uuidServerInfo)
log.BootInfof("[webserver.startWebServer] %s%s", serverInfo, uuidServerInfo)
if config.Mode == settings.MODE_PRODUCTION {
gin.SetMode(gin.ReleaseMode)
@@ -89,6 +89,7 @@ func startWebServer(c *cli.Context) error {
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
}
router.NoRoute(bindApi(api.Default.ApiNotFound))
@@ -144,6 +145,14 @@ func startWebServer(c *cli.Context) error {
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
if config.AvatarProvider == settings.InternalAvatarProvider {
avatarRoute := router.Group("/avatar")
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
avatarRoute.GET("/:fileName", bindImage(api.Users.UserGetAvatarHandler))
}
}
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
if config.Mode == settings.MODE_DEVELOPMENT {
@@ -160,9 +169,17 @@ func startWebServer(c *cli.Context) error {
config.MapProvider == settings.OpenTopoMapProvider ||
config.MapProvider == settings.OPNVKarteMapProvider ||
config.MapProvider == settings.CyclOSMMapProvider ||
config.MapProvider == settings.TomTomMapProvider {
config.MapProvider == settings.CartoDBMapProvider ||
config.MapProvider == settings.TomTomMapProvider ||
config.MapProvider == settings.TianDiTuProvider ||
config.MapProvider == settings.CustomProvider {
proxyRoute.GET("/map/tile/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapTileImageProxyHandler))
}
if config.MapProvider == settings.TianDiTuProvider ||
(config.MapProvider == settings.CustomProvider && config.CustomMapTileServerAnnotationLayerUrl != "") {
proxyRoute.GET("/map/annotation/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapAnnotationImageProxyHandler))
}
}
}
@@ -178,7 +195,7 @@ func startWebServer(c *cli.Context) error {
qrCodeRoute.Use(bindMiddleware(middlewares.RequestId(config)))
{
qrCodeCacheStore := persistence.NewInMemoryStore(time.Minute)
qrCodeRoute.GET("/mobile_url.png", bindCachedPngImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore))
qrCodeRoute.GET("/mobile_url.png", bindCachedImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore))
}
apiRoute := router.Group("/api")
@@ -236,11 +253,16 @@ func startWebServer(c *cli.Context) error {
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config))
if config.AvatarProvider == settings.InternalAvatarProvider {
apiV1Route.POST("/users/avatar/update.json", bindApi(api.Users.UserUpdateAvatarHandler))
apiV1Route.POST("/users/avatar/remove.json", bindApi(api.Users.UserRemoveAvatarHandler))
}
if config.EnableUserVerifyEmail {
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
}
// Two Factor Authorization
// 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))
@@ -254,7 +276,8 @@ func startWebServer(c *cli.Context) error {
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
if config.EnableDataExport {
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataToEzbookkeepingCSVHandler))
apiV1Route.GET("/data/export.tsv", bindTsv(api.DataManagements.ExportDataToEzbookkeepingTSVHandler))
}
// Accounts
@@ -271,8 +294,8 @@ func startWebServer(c *cli.Context) error {
apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler))
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
apiV1Route.GET("/transactions/amounts/by_month.json", bindApi(api.Transactions.TransactionMonthAmountsHandler))
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
@@ -297,6 +320,15 @@ func startWebServer(c *cli.Context) error {
apiV1Route.POST("/transaction/tags/move.json", bindApi(api.TransactionTags.TagMoveHandler))
apiV1Route.POST("/transaction/tags/delete.json", bindApi(api.TransactionTags.TagDeleteHandler))
// Transaction Templates
apiV1Route.GET("/transaction/templates/list.json", bindApi(api.TransactionTemplates.TemplateListHandler))
apiV1Route.GET("/transaction/templates/get.json", bindApi(api.TransactionTemplates.TemplateGetHandler))
apiV1Route.POST("/transaction/templates/add.json", bindApi(api.TransactionTemplates.TemplateCreateHandler))
apiV1Route.POST("/transaction/templates/modify.json", bindApi(api.TransactionTemplates.TemplateModifyHandler))
apiV1Route.POST("/transaction/templates/hide.json", bindApi(api.TransactionTemplates.TemplateHideHandler))
apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler))
apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler))
// Exchange Rates
apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler))
}
@@ -305,20 +337,20 @@ func startWebServer(c *cli.Context) error {
listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort)
if config.Protocol == settings.SCHEME_SOCKET {
log.BootInfof("[server.startWebServer] will run at socks:%s", config.UnixSocketPath)
log.BootInfof("[webserver.startWebServer] will run at socks:%s", config.UnixSocketPath)
err = router.RunUnix(config.UnixSocketPath)
} else if config.Protocol == settings.SCHEME_HTTP {
log.BootInfof("[server.startWebServer] will run at http://%s", listenAddr)
log.BootInfof("[webserver.startWebServer] will run at http://%s", listenAddr)
err = router.Run(listenAddr)
} else if config.Protocol == settings.SCHEME_HTTPS {
log.BootInfof("[server.startWebServer] will run at https://%s", listenAddr)
log.BootInfof("[webserver.startWebServer] will run at https://%s", listenAddr)
err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile)
} else {
err = errs.ErrInvalidProtocol
}
if err != nil {
log.BootErrorf("[server.startWebServer] cannot start, because %s", err)
log.BootErrorf("[webserver.startWebServer] cannot start, because %s", err)
return err
}
@@ -374,15 +406,41 @@ 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) {
func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, _, err := fn(c)
result, fileName, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, "img/png", "", result)
utils.PrintDataSuccessResult(c, "text/tab-separated-values", fileName, result)
}
}
}
func bindImage(fn core.ImageHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, contentType, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, contentType, "", result)
}
}
}
func bindCachedImage(fn core.ImageHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, contentType, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, contentType, "", result)
}
})
}
+126 -21
View File
@@ -25,7 +25,7 @@ root_url = %(protocol)s://%(domain)s:%(http_port)s/
cert_file =
cert_key_file =
# Unix socket path, for "socket" only
# Unix socket path, for "socket" protocol only
unix_socket =
# Static file root path (relative or absolute path)
@@ -47,10 +47,10 @@ name = ezbookkeeping
user = root
passwd =
# For "postgres" only, Either "disable", "require" or "verify-full"
# For "postgres" database only, Either "disable", "require" or "verify-full"
ssl_mode = disable
# For "sqlite3" only, db file path (relative or absolute path)
# For "sqlite3" database only, database file path (relative or absolute path)
db_path = data/ezbookkeeping.db
# Max idle connection number (0 - 65535, 0 means no idle connections are retained), default is 2
@@ -89,33 +89,88 @@ mode = console file
# Either "debug", "info", "warn", "error", default is "info"
level = info
# For "file" only, log file path (relative or absolute path)
# For "file" mode only, log file path (relative or absolute path)
log_path = log/ezbookkeeping.log
# For "file" only, request log file path (relative or absolute path). Leave blank if you want to write request log in default log file
request_log_path =
# For "file" only, query log file path (relative or absolute path). Leave blank if you want to write query log in default log file
query_log_path =
# For "file" only, whether rotate the log files
log_file_rotate = false
# For "file" only, maximum size (1 - 4294967295 bytes) of the log file before it gets rotated
log_file_max_size = 104857600
# For "file" only, maximum number of days to retain old log files. Set to 0 to retain all logs
log_file_max_days = 7
[storage]
# Object storage type, supports "local_filesystem" and "minio" currently
type = local_filesystem
# For "local_filesystem" storage only, the storage root path (relative or absolute path)
local_filesystem_path = storage/
# For "minio" storage only, the minio connection configuration
minio_endpoint = 127.0.0.1:9000
minio_location =
minio_access_key_id =
minio_secret_access_key =
# For "minio" storage only, whether enable ssl for minio connection
minio_use_ssl = false
# For "minio" storage only, set to true to skip tls verification when connect minio
minio_skip_tls_verify = false
# For "minio" storage only, the minio bucket
minio_bucket = ezbookkeeping
# For "minio" storage only, the root path to store files in minio
minio_root_path = /
[uuid]
# Uuid generator type, supports "internal" currently
generator_type = internal
# For "internal" only, each server must have unique id (0 - 255)
# For "internal" uuid generator only, each server must have unique id (0 - 255)
server_id = 0
[duplicate_checker]
# Duplicate checker type, supports "in_memory" currently
checker_type = in_memory
# For "in_memory" duplicate checker only, cleanup expired data interval seconds (1 - 4294967295), default is 60 (1 minutes)
cleanup_interval = 60
# The minimum interval seconds (0 - 4294967295) between duplicate submissions on the same page (exiting and re-entering the page is considered as a new session)
# Set to 0 to disable duplicate checker for new data submissions, default is 300 (5 minutes)
duplicate_submissions_interval = 300
[security]
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
secret_key =
# Set to true to enable two factor authorization
# Set to true to enable two-factor authorization
enable_two_factor = true
# Token expired seconds (0 - 4294967295), default is 2592000 (30 days)
# Token expired seconds (60 - 4294967295), default is 2592000 (30 days)
token_expired_time = 2592000
# Temporary token expired seconds (0 - 4294967295), default is 300 (5 minutes)
# Token minimum refresh interval (0 - 4294967295), the value should be less than token expired time
# Set to 0 to refresh the token every time when refreshing the front end, default is 86400 (1 day)
token_min_refresh_interval = 86400
# Temporary token expired seconds (60 - 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 seconds (60 - 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 seconds (60 - 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
@@ -138,14 +193,36 @@ enable_forget_password = true
forget_password_require_email_verify = false
# User avatar provider, supports the following types:
# "internal": Use the internal object storage to store user avatar (refer to "storage" settings), supports updating avatar by user self
# "gravatar": https://gravatar.com
# Leave blank if you want to disable user avatar
avatar_provider =
avatar_provider = internal
[data]
# Set to true to allow users to export their data
enable_export = true
[notification]
# Set to true to display custom notification in home page every time users register
enable_notification_after_register = false
# The notification content displayed each time users register, it supports multi-language configuration
# Add an underscore and a language tag after the setting key to configure the notification content in that language, the same below
# For example, after_login_notification_content_zh_hans means the notification content in Simplified Chinese
after_register_notification_content =
# Set to true to display custom notification in home page every time users login
enable_notification_after_login = false
# The notification content displayed each time users log in, it supports multi-language configuration
after_login_notification_content =
# Set to true to display custom notification in home page every time users open the app
enable_notification_after_open = false
# The notification content displayed each time users open the app, it supports multi-language configuration
after_open_notification_content =
[map]
# Map provider, supports the following types:
# "openstreetmap": https://www.openstreetmap.org
@@ -153,41 +230,65 @@ enable_export = true
# "opentopomap": https://opentopomap.org
# "opnvkarte": https://publictransportmap.org
# "cyclosm": https://www.cyclosm.org
# "cartodb": https://carto.com/basemaps
# "tomtom": https://www.tomtom.com
# "tianditu": https://www.tianditu.gov.cn
# "googlemap": https://map.google.com
# "baidumap": https://map.baidu.com
# "amap": https://amap.com
# "custom": custom map tile server url
# Leave blank if you want to disable map
map_provider = openstreetmap
# Set to true to use the ezbookkeeping server to proxy map data requests, for "openstreetmap", "openstreetmap_humanitarian", "opentopomap", "opnvkarte", "cyclosm" or "tomtom"
# Set to true to use the ezbookkeeping server to forward map data requests, for "openstreetmap", "openstreetmap_humanitarian", "opentopomap", "opnvkarte", "cyclosm", "cartodb", "tomtom", "tianditu" or "custom"
map_data_fetch_proxy = false
# For "tomtom" only, TomTom map API key, please visit https://developer.tomtom.com/how-to-get-tomtom-api-key
# Proxy for ezbookkeeping server requesting original map data when map_data_fetch_proxy is set to true, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
proxy = system
# For "tomtom" map provider only, TomTom map API key, please visit https://developer.tomtom.com/how-to-get-tomtom-api-key for more information
tomtom_map_api_key =
# For "googlemap" only, Google map JavaScript API key, please visit https://developers.google.com/maps/get-started for more information
# For "tianditu" map provider only, TianDiTu map application key, please visit https://console.tianditu.gov.cn/api/register for more information
tianditu_map_app_key =
# For "googlemap" map provider 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
# For "baidumap" map provider 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
# For "amap" map provider 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:
# For "amap" map provider 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
# For "amap" map provider 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"
# For "amap" map provider 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 =
# For "custom" map provider only, the tile layer url of custom map tile server, supports {x}, {y} (coordinates) and {z} (zoom level) placeholders, like "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
custom_map_tile_server_url =
# For "custom" map provider only, the optional annotation layer url of custom map tile server, supports {x}, {y} (coordinates) and {z} (zoom level) placeholders
custom_map_tile_server_annotation_url =
# For "custom" map provider only, the min zoom level (0 - 255) for custom map tile server, default is 1
custom_map_tile_server_min_zoom_level = 1
# For "custom" map provider only, the max zoom level (0 - 255) for custom map tile server, default is 18
custom_map_tile_server_max_zoom_level = 18
# For "custom" map provider only, the default zoom level (0 - 255) for custom map tile server, default is 14
custom_map_tile_server_default_zoom_level = 14
[exchange_rates]
# Exchange rates data source, supports the following types:
# "euro_central_bank"
@@ -198,8 +299,12 @@ amap_api_external_proxy_url =
# "monetary_authority_of_singapore"
data_source = euro_central_bank
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds), default is 10000 (10 seconds)
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
# Set to 0 to disable timeout for requesting exchange rates data, default is 10000 (10 seconds)
request_timeout = 10000
# Set to true skip tls verification when request exchange rates data
# Proxy for ezbookkeeping server requesting exchange rates data, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
proxy = system
# Set to true to skip tls verification when request exchange rates data
skip_tls_verify = false
+42 -29
View File
@@ -1,65 +1,78 @@
module github.com/mayswind/ezbookkeeping
go 1.20
go 1.21
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.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/boombuler/barcode v1.0.2
github.com/gin-contrib/cache v1.3.0
github.com/gin-contrib/gzip v1.0.1
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.22.0
github.com/go-sql-driver/mysql v1.8.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.16
github.com/mattn/go-sqlite3 v1.14.22
github.com/minio/minio-go/v7 v7.0.74
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/otp v1.4.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.25.7
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.1
github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/crypto v0.12.0
golang.org/x/crypto v0.25.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/mail.v2 v2.3.1
xorm.io/xorm v1.3.2
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // 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/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.1 // 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
)
+87 -683
View File
@@ -1,778 +1,182 @@
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=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/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/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cache v1.3.0 h1:wEEw38uvb4rTraQJVpd9ex4ZotXNlc0fSaSUsuPXS/w=
github.com/gin-contrib/cache v1.3.0/go.mod h1:EA63LrWGI5vwSI95TS5fgBrtxZ1tM2NKx+NrEeyEDcU=
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
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.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/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/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0=
github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/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/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE=
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
+2643 -2850
View File
File diff suppressed because it is too large Load Diff
+26 -27
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "0.4.0",
"version": "0.5.0",
"private": true,
"repository": {
"type": "git",
@@ -18,47 +18,46 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"@mdi/js": "^7.2.96",
"@vuepic/vue-datepicker": "^5.4.0",
"axios": "^1.4.0",
"@mdi/js": "^7.4.47",
"@vuepic/vue-datepicker": "^8.8.1",
"axios": "^1.7.2",
"cbor-js": "^0.1.0",
"clipboard": "^2.0.11",
"crypto-js": "^4.1.1",
"crypto-js": "^4.2.0",
"dom7": "^4.0.6",
"echarts": "^5.4.3",
"framework7": "^8.3.0",
"echarts": "^5.5.1",
"framework7": "^8.3.3",
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.3.0",
"framework7-vue": "^8.3.3",
"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",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"pinia": "^2.1.7",
"register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^10.2.0",
"ua-parser-js": "^1.0.35",
"vue": "^3.3.4",
"vue-echarts": "^6.6.1",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.4",
"vue3-perfect-scrollbar": "^1.6.1",
"ua-parser-js": "^1.0.38",
"vue": "^3.4.31",
"vue-echarts": "^6.7.3",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.0",
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.3.16"
"vuetify": "^3.6.11"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.4",
"@vitejs/plugin-vue": "^5.0.5",
"cross-env": "^7.0.3",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0",
"git-rev-sync": "^3.0.2",
"postcss-preset-env": "^9.1.1",
"sass": "^1.66.1",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-vuetify": "^1.0.2"
"postcss-preset-env": "^9.5.16",
"sass": "^1.77.6",
"vite": "^5.3.3",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-vuetify": "^2.0.3"
},
"browserslist": [
"> 1%",
+55 -5
View File
@@ -4,10 +4,13 @@ import (
"sort"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"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"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
@@ -143,9 +146,14 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
return nil, errs.ErrClientTimezoneOffsetInvalid
}
if accountCreateReq.Category < models.ACCOUNT_CATEGORY_CASH || accountCreateReq.Category > models.ACCOUNT_CATEGORY_INVESTMENT {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account category invalid, category is %d", accountCreateReq.Category)
return nil, errs.ErrAccountCategoryInvalid
}
if accountCreateReq.Type == models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
if len(accountCreateReq.SubAccounts) > 0 {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot have any sub accounts")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot have any sub-accounts")
return nil, errs.ErrAccountCannotHaveSubAccounts
}
@@ -155,7 +163,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
}
} else if accountCreateReq.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
if len(accountCreateReq.SubAccounts) < 1 {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account does not have any sub accounts")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account does not have any sub-accounts")
return nil, errs.ErrAccountHaveNoSubAccount
}
@@ -173,17 +181,17 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
subAccount := accountCreateReq.SubAccounts[i]
if subAccount.Category != accountCreateReq.Category {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] category of sub account not equals to parent")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] category of sub-account not equals to parent")
return nil, errs.ErrSubAccountCategoryNotEqualsToParent
}
if subAccount.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub account type invalid")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub-account type invalid")
return nil, errs.ErrSubAccountTypeInvalid
}
if subAccount.Currency == validators.ParentAccountCurrencyPlaceholder {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub account cannot set currency placeholder")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub-account cannot set currency placeholder")
return nil, errs.ErrAccountCurrencyInvalid
}
}
@@ -203,6 +211,42 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
if settings.Container.Current.EnableDuplicateSubmissionsCheck && accountCreateReq.ClientSessionId != "" {
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId)
if found {
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] another account \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
accountId, err := utils.StringToInt64(remark)
if err == nil {
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountId)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get existed account \"id:%d\" for user \"uid:%d\", because %s", accountId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
mainAccount, exists := accountMap[accountId]
if !exists {
return nil, errs.ErrOperationFailed
}
accountInfoResp := mainAccount.ToAccountInfoResponse()
for i := 0; i < len(accountAndSubAccounts); i++ {
if accountAndSubAccounts[i].ParentAccountId == mainAccount.AccountId {
subAccountResp := accountAndSubAccounts[i].ToAccountInfoResponse()
accountInfoResp.SubAccounts = append(accountInfoResp.SubAccounts, subAccountResp)
}
}
return accountInfoResp, nil
}
}
}
err = a.accounts.CreateAccounts(c, mainAccount, childrenAccounts, utcOffset)
if err != nil {
@@ -212,6 +256,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId, utils.Int64ToString(mainAccount.AccountId))
accountInfoResp := mainAccount.ToAccountInfoResponse()
if len(childrenAccounts) > 0 {
@@ -235,6 +280,11 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if accountModifyReq.Category < models.ACCOUNT_CATEGORY_CASH || accountModifyReq.Category > models.ACCOUNT_CATEGORY_INVESTMENT {
log.WarnfWithRequestId(c, "[accounts.AccountModifyHandler] account category invalid, category is %d", accountModifyReq.Category)
return nil, errs.ErrAccountCategoryInvalid
}
uid := c.GetCurrentUid()
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountModifyReq.Id)
+14 -13
View File
@@ -77,7 +77,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error)
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())
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two-factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
}
@@ -104,7 +104,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error)
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)
authResp := a.getAuthResponse(token, twoFactorEnable, user)
authResp := a.getAuthResponse(c, token, twoFactorEnable, user)
return authResp, nil
}
@@ -122,7 +122,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *er
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())
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
@@ -165,9 +165,9 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *er
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)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two-factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
authResp := a.getAuthResponse(token, false, user)
authResp := a.getAuthResponse(c, token, false, user)
return authResp, nil
}
@@ -185,7 +185,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
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())
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
@@ -213,7 +213,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
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())
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
}
@@ -234,16 +234,17 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
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)
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)
authResp := a.getAuthResponse(token, false, user)
authResp := a.getAuthResponse(c, token, false, user)
return authResp, nil
}
func (a *AuthorizationsApi) getAuthResponse(token string, need2FA bool, user *models.User) *models.AuthResponse {
func (a *AuthorizationsApi) getAuthResponse(c *core.Context, token string, need2FA bool, user *models.User) *models.AuthResponse {
return &models.AuthResponse{
Token: token,
Need2FA: need2FA,
User: user.ToUserBasicInfo(),
Token: token,
Need2FA: need2FA,
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()),
}
}
+126 -90
View File
@@ -19,103 +19,40 @@ const pageCountForDataExport = 1000
// DataManagementsApi represents data management api
type DataManagementsApi struct {
exporter *converters.EzBookKeepingCSVFileExporter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
templates *services.TransactionTemplateService
}
// Initialize a data management api singleton instance
var (
DataManagements = &DataManagementsApi{
exporter: &converters.EzBookKeepingCSVFileExporter{},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
templates: services.TransactionTemplates,
}
)
// ExportDataHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string, *errs.Error) {
if !settings.Container.Current.EnableDataExport {
return nil, "", errs.ErrDataExportNotAllowed
}
// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataToEzbookkeepingCSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
return a.getExportedFileContent(c, "csv")
}
timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, "", errs.ErrUserNotFound
}
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(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(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(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())
return nil, "", errs.ErrOperationFailed
}
accountMap := a.accounts.GetAccountMapByList(accounts)
categoryMap := a.categories.GetCategoryMapByList(categories)
tagMap := a.tags.GetTagMapByList(tags)
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())
return nil, "", errs.ErrOperationFailed
}
result, err := a.exporter.ToExportedContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
fileName := a.getFileName(user, timezone)
return result, fileName, nil
// ExportDataToEzbookkeepingTSVHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataToEzbookkeepingTSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
return a.getExportedFileContent(c, "tsv")
}
// DataStatisticsHandler returns user data statistics
@@ -149,11 +86,19 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.
return nil, errs.ErrOperationFailed
}
totalTransactionTemplateCount, err := a.templates.GetTotalNormalTemplateCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction template count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
dataStatisticsResp := &models.DataStatisticsResponse{
TotalAccountCount: totalAccountCount,
TotalTransactionCategoryCount: totalTransactionCategoryCount,
TotalTransactionTagCount: totalTransactionTagCount,
TotalTransactionCount: totalTransactionCount,
TotalTransactionTemplateCount: totalTransactionTemplateCount,
}
return dataStatisticsResp, nil
@@ -205,15 +150,106 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.templates.DeleteAllTemplates(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction templates, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
return true, nil
}
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location) string {
func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType string) ([]byte, string, *errs.Error) {
if !settings.Container.Current.EnableDataExport {
return nil, "", errs.ErrDataExportNotAllowed
}
timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, "", errs.ErrUserNotFound
}
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(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(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
}
tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(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())
return nil, "", errs.ErrOperationFailed
}
accountMap := a.accounts.GetAccountMapByList(accounts)
categoryMap := a.categories.GetCategoryMapByList(categories)
tagMap := a.tags.GetTagMapByList(tags)
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())
return nil, "", errs.ErrOperationFailed
}
var dataExporter converters.DataConverter
if fileType == "tsv" {
dataExporter = a.ezBookKeepingTsvExporter
} else {
dataExporter = a.ezBookKeepingCsvExporter
}
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
fileName := a.getFileName(user, timezone, fileType)
return result, fileName, nil
}
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string {
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
currentTime = strings.Replace(currentTime, "-", "_", -1)
currentTime = strings.Replace(currentTime, " ", "_", -1)
currentTime = strings.Replace(currentTime, ":", "_", -1)
return fmt.Sprintf("%s_%s.csv", user.Username, currentTime)
return fmt.Sprintf("%s_%s.%s", user.Username, currentTime, fileExtension)
}
+7 -1
View File
@@ -2,6 +2,7 @@ package api
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"sort"
@@ -13,6 +14,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// ExchangeRatesApi represents exchange rate api
@@ -34,6 +36,7 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *err
uid := c.GetCurrentUid()
transport := http.DefaultTransport.(*http.Transport).Clone()
utils.SetProxyUrl(transport, settings.Container.Current.ExchangeRatesProxy)
if settings.Container.Current.ExchangeRatesSkipTLSVerify {
transport.TLSClientConfig = &tls.Config{
@@ -50,7 +53,10 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *err
exchangeRateResps := make([]*models.LatestExchangeRateResponse, 0, len(urls))
for i := 0; i < len(urls); i++ {
resp, err := client.Get(urls[i])
req, _ := http.NewRequest("GET", urls[i], nil)
req.Header.Set("User-Agent", fmt.Sprintf("ezBookkeeping/%s ", settings.Version))
resp, err := client.Do(req)
if err != nil {
log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error())
+88 -32
View File
@@ -1,7 +1,6 @@
package api
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
@@ -10,14 +9,18 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
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}
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
const openStreetMapHumanitarianStyleTileImageUrlFormat = "https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png" // https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png
const openTopoMapTileImageUrlFormat = "https://tile.opentopomap.org/{z}/{x}/{y}.png" // https://tile.opentopomap.org/{z}/{x}/{y}.png
const opnvKarteMapTileImageUrlFormat = "https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png" // https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png
const cyclOSMMapTileImageUrlFormat = "https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png" // https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png
const cartoDBMapTileImageUrlFormat = "https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{scale}.png" // https://{s}.basemaps.cartocdn.com/{style}/{z}/{x}/{y}{scale}.png
const tomtomMapTileImageUrlFormat = "https://api.tomtom.com/map/1/tile/basic/main/{z}/{x}/{y}.png" // https://api.tomtom.com/map/{versionNumber}/tile/{layer}/{style}/{z}/{x}/{y}.png?key={key}&language={language}
const tianDiTuMapTileImageUrlFormat = "https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" // https://t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={key}
const tianDiTuMapAnnotationUrlFormat = "https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" // https://t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={key}
// MapImageProxy represents map image proxy
type MapImageProxy struct {
@@ -30,36 +33,86 @@ var (
// MapTileImageProxyHandler returns map tile image
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
return p.mapImageProxyHandler(c, func(c *core.Context, mapProvider string) (string, *errs.Error) {
if mapProvider == settings.OpenStreetMapProvider {
return openStreetMapTileImageUrlFormat, nil
} else if mapProvider == settings.OpenStreetMapHumanitarianStyleProvider {
return openStreetMapHumanitarianStyleTileImageUrlFormat, nil
} else if mapProvider == settings.OpenTopoMapProvider {
return openTopoMapTileImageUrlFormat, nil
} else if mapProvider == settings.OPNVKarteMapProvider {
return opnvKarteMapTileImageUrlFormat, nil
} else if mapProvider == settings.CyclOSMMapProvider {
return cyclOSMMapTileImageUrlFormat, nil
} else if mapProvider == settings.CartoDBMapProvider {
return cartoDBMapTileImageUrlFormat, nil
} else if mapProvider == settings.TomTomMapProvider {
targetUrl := tomtomMapTileImageUrlFormat + "?key=" + settings.Container.Current.TomTomMapAPIKey
language := c.Query("language")
if language != "" {
targetUrl = targetUrl + "&language=" + language
}
return targetUrl, nil
} else if mapProvider == settings.TianDiTuProvider {
return tianDiTuMapTileImageUrlFormat + "&tk=" + settings.Container.Current.TianDiTuAPIKey, nil
} else if mapProvider == settings.CustomProvider {
return settings.Container.Current.CustomMapTileServerTileLayerUrl, nil
}
return "", errs.ErrParameterInvalid
})
}
// MapAnnotationImageProxyHandler returns map annotation image
func (p *MapImageProxy) MapAnnotationImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
return p.mapImageProxyHandler(c, func(c *core.Context, mapProvider string) (string, *errs.Error) {
if mapProvider == settings.TianDiTuProvider {
return tianDiTuMapAnnotationUrlFormat + "&tk=" + settings.Container.Current.TianDiTuAPIKey, nil
} else if mapProvider == settings.CustomProvider {
return settings.Container.Current.CustomMapTileServerAnnotationLayerUrl, nil
}
return "", errs.ErrParameterInvalid
})
}
func (p *MapImageProxy) mapImageProxyHandler(c *core.Context, fn func(c *core.Context, mapProvider string) (string, *errs.Error)) (*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
if mapProvider != settings.Container.Current.MapProvider {
return nil, errs.ErrMapProviderNotCurrent
}
director := func(req *http.Request) {
zoomLevel := c.Param("zoomLevel")
coordinateX := c.Param("coordinateX")
fileName := c.Param("fileName")
zoomLevel := c.Param("zoomLevel")
coordinateX := c.Param("coordinateX")
fileName := c.Param("fileName")
fileNameParts := strings.Split(fileName, ".")
coordinateY := fileNameParts[0]
scale := c.Query("scale")
imageRawUrl := fmt.Sprintf(targetUrl, zoomLevel, coordinateX, fileName)
if len(fileNameParts) != 2 || fileNameParts[len(fileNameParts)-1] != "png" {
return nil, errs.ErrImageExtensionNotSupported
}
var err *errs.Error
targetUrl, err = fn(c, mapProvider)
if err != nil {
return nil, err
}
transport := http.DefaultTransport.(*http.Transport).Clone()
utils.SetProxyUrl(transport, settings.Container.Current.MapProxy)
director := func(req *http.Request) {
imageRawUrl := targetUrl
imageRawUrl = strings.Replace(imageRawUrl, "{z}", zoomLevel, -1)
imageRawUrl = strings.Replace(imageRawUrl, "{x}", coordinateX, -1)
imageRawUrl = strings.Replace(imageRawUrl, "{y}", coordinateY, -1)
imageRawUrl = strings.Replace(imageRawUrl, "{scale}", scale, -1)
imageUrl, _ := url.Parse(imageRawUrl)
req.URL = imageUrl
@@ -67,5 +120,8 @@ func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.Rev
req.Host = imageUrl.Host
}
return &httputil.ReverseProxy{Director: director}, nil
return &httputil.ReverseProxy{
Transport: transport,
Director: director,
}, nil
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.Context) ([]byte, string, *e
return nil, "", errs.ErrOperationFailed
}
return data, "", nil
return data, "image/png", nil
}
func (a *QrCodesApi) generateUrlQrCode(c *core.Context, url string) ([]byte, *errs.Error) {
+41 -6
View File
@@ -2,12 +2,14 @@ package api
import (
"sort"
"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"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -44,8 +46,7 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (any, *errs.Error) {
TokenId: a.tokens.GenerateTokenId(token),
TokenType: token.TokenType,
UserAgent: token.UserAgent,
CreatedAt: token.CreatedUnixTime,
ExpiredAt: token.ExpiredUnixTime,
LastSeen: token.LastSeenUnixTime,
}
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
@@ -176,6 +177,40 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
return nil, errs.ErrUserNotFound
}
now := time.Now().Unix()
oldTokenClaims := c.GetTokenClaims()
if now-oldTokenClaims.IssuedAt < int64(settings.Container.Current.TokenMinRefreshInterval) {
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid)
userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.TokenRefreshHandler] parse user token id failed, because %s", err.Error())
} else {
tokenRecord := &models.TokenRecord{
Uid: oldTokenClaims.Uid,
UserTokenId: userTokenId,
CreatedUnixTime: oldTokenClaims.IssuedAt,
}
tokenId := a.tokens.GenerateTokenId(tokenRecord)
err = a.tokens.UpdateTokenLastSeen(c, tokenRecord)
if err != nil {
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to update last seen of token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
}
}
refreshResp := &models.TokenRefreshResponse{
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
}
return refreshResp, nil
}
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
@@ -183,7 +218,6 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
return nil, errs.Or(err, errs.ErrTokenGenerating)
}
oldTokenClaims := c.GetTokenClaims()
oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
oldTokenRecord := &models.TokenRecord{
Uid: uid,
@@ -197,9 +231,10 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
refreshResp := &models.TokenRefreshResponse{
NewToken: token,
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
User: user.ToUserBasicInfo(),
NewToken: token,
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
}
return refreshResp, nil
+68 -9
View File
@@ -6,10 +6,13 @@ import (
"github.com/gin-gonic/gin/binding"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"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"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TransactionCategoriesApi represents transaction category api
@@ -119,6 +122,28 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
if settings.Container.Current.EnableDuplicateSubmissionsCheck && categoryCreateReq.ClientSessionId != "" {
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId)
if found {
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] another category \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
categoryId, err := utils.StringToInt64(remark)
if err == nil {
category, err = a.categories.GetCategoryByCategoryId(c, uid, categoryId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get existed category \"id:%d\" for user \"uid:%d\", because %s", categoryId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
categoryResp := category.ToTransactionCategoryInfoResponse()
return categoryResp, nil
}
}
}
err = a.categories.CreateCategory(c, category)
if err != nil {
@@ -128,6 +153,7 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId)
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId, utils.Int64ToString(category.CategoryId))
categoryResp := category.ToTransactionCategoryInfoResponse()
return categoryResp, nil
@@ -173,16 +199,18 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
}
newCategory := &models.TransactionCategory{
CategoryId: category.CategoryId,
Uid: uid,
Name: categoryModifyReq.Name,
Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment,
Hidden: categoryModifyReq.Hidden,
CategoryId: category.CategoryId,
Uid: uid,
ParentCategoryId: categoryModifyReq.ParentId,
Name: categoryModifyReq.Name,
Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment,
Hidden: categoryModifyReq.Hidden,
}
if newCategory.Name == category.Name &&
if newCategory.ParentCategoryId == category.ParentCategoryId &&
newCategory.Name == category.Name &&
newCategory.Icon == category.Icon &&
newCategory.Color == category.Color &&
newCategory.Comment == category.Comment &&
@@ -190,6 +218,38 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
return nil, errs.ErrNothingWillBeUpdated
}
if category.ParentCategoryId == 0 && newCategory.ParentCategoryId != 0 {
return nil, errs.Or(err, errs.ErrNotAllowChangePrimaryTransactionCategoryToSecondary)
}
if category.ParentCategoryId != 0 && newCategory.ParentCategoryId == 0 {
return nil, errs.Or(err, errs.ErrNotAllowChangeSecondaryTransactionCategoryToPrimary)
}
if newCategory.ParentCategoryId != category.ParentCategoryId {
fromPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, category.ParentCategoryId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get old primary category \"id:%d\" of category \"id:%d\" for user \"uid:%d\", because %s", category.ParentCategoryId, categoryModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
toPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, newCategory.ParentCategoryId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get new primary category \"id:%d\" of category \"id:%d\" for user \"uid:%d\", because %s", newCategory.ParentCategoryId, categoryModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if fromPrimaryCategory.Type != toPrimaryCategory.Type {
return nil, errs.Or(err, errs.ErrNotAllowChangePrimaryTransactionType)
}
if toPrimaryCategory.ParentCategoryId != 0 {
return nil, errs.Or(err, errs.ErrNotAllowUseSecondaryTransactionAsPrimaryCategory)
}
}
err = a.categories.ModifyCategory(c, newCategory)
if err != nil {
@@ -200,7 +260,6 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
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()
+6 -6
View File
@@ -150,7 +150,7 @@ func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error)
err := c.ShouldBindJSON(&tagHideReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_tags.CategoryHideHandler] parse request failed, because %s", err.Error())
log.WarnfWithRequestId(c, "[transaction_tags.TagHideHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
@@ -158,11 +158,11 @@ func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error)
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())
log.ErrorfWithRequestId(c, "[transaction_tags.TagHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_tags.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, tagHideReq.Id)
log.InfofWithRequestId(c, "[transaction_tags.TagHideHandler] user \"uid:%d\" has hidden tag \"id:%d\"", uid, tagHideReq.Id)
return true, nil
}
@@ -172,7 +172,7 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error)
err := c.ShouldBindJSON(&tagMoveReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_tags.CategoryMoveHandler] parse request failed, because %s", err.Error())
log.WarnfWithRequestId(c, "[transaction_tags.TagMoveHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
@@ -193,11 +193,11 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error)
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())
log.ErrorfWithRequestId(c, "[transaction_tags.TagMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_tags.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
log.InfofWithRequestId(c, "[transaction_tags.TagMoveHandler] user \"uid:%d\" has moved tags", uid)
return true, nil
}
+321
View File
@@ -0,0 +1,321 @@
package api
import (
"sort"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"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"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TransactionTemplatesApi represents transaction template api
type TransactionTemplatesApi struct {
templates *services.TransactionTemplateService
}
// Initialize a transaction template api singleton instance
var (
TransactionTemplates = &TransactionTemplatesApi{
templates: services.TransactionTemplates,
}
)
// TemplateListHandler returns transaction template list of current user
func (a *TransactionTemplatesApi) TemplateListHandler(c *core.Context) (any, *errs.Error) {
var templateListReq models.TransactionTemplateListRequest
err := c.ShouldBindQuery(&templateListReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateListHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateListHandler] template type invalid, type is %d", templateListReq.TemplateType)
return nil, errs.ErrTransactionTemplateTypeInvalid
}
uid := c.GetCurrentUid()
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateListHandler] failed to get templates for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
templateResps := make(models.TransactionTemplateInfoResponseSlice, len(templates))
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
for i := 0; i < len(templates); i++ {
templateResps[i] = templates[i].ToTransactionTemplateInfoResponse(serverUtcOffset)
}
sort.Sort(templateResps)
return templateResps, nil
}
// TemplateGetHandler returns one specific transaction template of current user
func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.Context) (any, *errs.Error) {
var templateGetReq models.TransactionTemplateGetRequest
err := c.ShouldBindQuery(&templateGetReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateGetHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateGetHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateGetReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
return templateResp, nil
}
// TemplateCreateHandler saves a new transaction template by request parameters for current user
func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.Context) (any, *errs.Error) {
var templateCreateReq models.TransactionTemplateCreateRequest
err := c.ShouldBindJSON(&templateCreateReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] template type invalid, type is %d", templateCreateReq.TemplateType)
return nil, errs.ErrTransactionTemplateTypeInvalid
}
if templateCreateReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] transaction type invalid, type is %d", templateCreateReq.Type)
return nil, errs.ErrTransactionTypeInvalid
}
uid := c.GetCurrentUid()
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1)
if settings.Container.Current.EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" {
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId)
if found {
log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] another template \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
templateId, err := utils.StringToInt64(remark)
if err == nil {
template, err = a.templates.GetTemplateByTemplateId(c, uid, templateId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get existed template \"id:%d\" for user \"uid:%d\", because %s", templateId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
return templateResp, nil
}
}
}
err = a.templates.CreateTemplate(c, template)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to create template \"id:%d\" for user \"uid:%d\", because %s", template.TemplateId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] user \"uid:%d\" has created a new template \"id:%d\" successfully", uid, template.TemplateId)
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId))
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
return templateResp, nil
}
// TemplateModifyHandler saves an existed transaction template by request parameters for current user
func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *errs.Error) {
var templateModifyReq models.TransactionTemplateModifyRequest
err := c.ShouldBindJSON(&templateModifyReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if templateModifyReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateModifyReq.Type > models.TRANSACTION_TYPE_TRANSFER {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] transaction type invalid, type is %d", templateModifyReq.Type)
return nil, errs.ErrTransactionTypeInvalid
}
uid := c.GetCurrentUid()
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newTemplate := &models.TransactionTemplate{
TemplateId: template.TemplateId,
Uid: uid,
Name: templateModifyReq.Name,
Type: templateModifyReq.Type,
CategoryId: templateModifyReq.CategoryId,
AccountId: templateModifyReq.SourceAccountId,
TagIds: strings.Join(templateModifyReq.TagIds, ","),
Amount: templateModifyReq.SourceAmount,
RelatedAccountId: templateModifyReq.DestinationAccountId,
RelatedAccountAmount: templateModifyReq.DestinationAmount,
HideAmount: templateModifyReq.HideAmount,
Comment: templateModifyReq.Comment,
}
if newTemplate.Name == template.Name &&
newTemplate.Type == template.Type &&
newTemplate.CategoryId == template.CategoryId &&
newTemplate.AccountId == template.AccountId &&
newTemplate.TagIds == template.TagIds &&
newTemplate.Amount == template.Amount &&
newTemplate.RelatedAccountId == template.RelatedAccountId &&
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
newTemplate.HideAmount == template.HideAmount &&
newTemplate.Comment == template.Comment {
return nil, errs.ErrNothingWillBeUpdated
}
err = a.templates.ModifyTemplate(c, newTemplate)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to update template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateModifyHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id)
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
newTemplate.TemplateType = template.TemplateType
newTemplate.DisplayOrder = template.DisplayOrder
newTemplate.Hidden = template.Hidden
templateResp := newTemplate.ToTransactionTemplateInfoResponse(serverUtcOffset)
return templateResp, nil
}
// TemplateHideHandler hides an transaction template by request parameters for current user
func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.Context) (any, *errs.Error) {
var templateHideReq models.TransactionTemplateHideRequest
err := c.ShouldBindJSON(&templateHideReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateHideHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateHideHandler] failed to hide template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateHideHandler] user \"uid:%d\" has hidden template \"id:%d\"", uid, templateHideReq.Id)
return true, nil
}
// TemplateMoveHandler moves display order of existed transaction templates by request parameters for current user
func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.Context) (any, *errs.Error) {
var templateMoveReq models.TransactionTemplateMoveRequest
err := c.ShouldBindJSON(&templateMoveReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.CategoryMoveHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders))
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
newDisplayOrder := templateMoveReq.NewDisplayOrders[i]
template := &models.TransactionTemplate{
Uid: uid,
TemplateId: newDisplayOrder.Id,
DisplayOrder: newDisplayOrder.DisplayOrder,
}
templates[i] = template
}
err = a.templates.ModifyTemplateDisplayOrders(c, uid, templates)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateMoveHandler] failed to move templates for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateMoveHandler] user \"uid:%d\" has moved templates", uid)
return true, nil
}
// TemplateDeleteHandler deletes an existed transaction template by request parameters for current user
func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.Context) (any, *errs.Error) {
var templateDeleteReq models.TransactionTemplateDeleteRequest
err := c.ShouldBindJSON(&templateDeleteReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] failed to delete template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] user \"uid:%d\" has deleted template \"id:%d\"", uid, templateDeleteReq.Id)
return true, nil
}
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
return &models.TransactionTemplate{
Uid: uid,
TemplateType: templateCreateReq.TemplateType,
Name: templateCreateReq.Name,
Type: templateCreateReq.Type,
CategoryId: templateCreateReq.CategoryId,
AccountId: templateCreateReq.SourceAccountId,
TagIds: strings.Join(templateCreateReq.TagIds, ","),
Amount: templateCreateReq.SourceAmount,
RelatedAccountId: templateCreateReq.DestinationAccountId,
RelatedAccountAmount: templateCreateReq.DestinationAmount,
HideAmount: templateCreateReq.HideAmount,
Comment: templateCreateReq.Comment,
DisplayOrder: order,
}
}
+236 -145
View File
@@ -7,15 +7,15 @@ import (
orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"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"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const pageCountForLoadTransactionAmounts = 1000
// TransactionsApi represents transaction api
type TransactionsApi struct {
transactions *services.TransactionService
@@ -48,21 +48,33 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (any, *errs.E
uid := c.GetCurrentUid()
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionCountReq.AccountId, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionCountReq.AccountIds, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionCountReq.CategoryId, uid)
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionCountReq.CategoryIds, 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(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, transactionCountReq.Keyword)
var allTagIds []int64
noTags := transactionCountReq.TagIds == "none"
if !noTags {
allTagIds, err = a.getTagIds(transactionCountReq.TagIds)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -104,24 +116,36 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (any, *errs.Er
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountId, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountIds, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryId, uid)
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryIds, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := transactionListReq.TagIds == "none"
if !noTags {
allTagIds, err = a.getTagIds(transactionListReq.TagIds)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
var totalCount int64
if transactionListReq.WithCount {
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword)
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -129,7 +153,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (any, *errs.Er
}
}
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, 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())
@@ -195,21 +219,33 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (any, *er
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountId, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountIds, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryId, uid)
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryIds, 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(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword)
var allTagIds []int64
noTags := transactionListReq.TagIds == "none"
if !noTags {
allTagIds, err = a.getTagIds(transactionListReq.TagIds)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, 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())
@@ -241,8 +277,15 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (any, *e
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime)
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, utcOffset, statisticReq.UseTransactionTimezone)
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())
@@ -268,6 +311,64 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (any, *e
return statisticResp, nil
}
// TransactionStatisticsTrendsHandler returns transaction statistics trends of current user
func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.Context) (any, *errs.Error) {
var statisticTrendsReq models.TransactionStatisticTrendsRequest
err := c.ShouldBindQuery(&statisticTrendsReq)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsTrendsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsTrendsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
startYear, startMonth, endYear, endMonth, err := statisticTrendsReq.GetNumericYearMonthRange()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsTrendsHandler] cannot parse year month, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
uid := c.GetCurrentUid()
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyIncomeAndExpense(c, uid, startYear, startMonth, endYear, endMonth, utcOffset, statisticTrendsReq.UseTransactionTimezone)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticTrendsResp := make(models.TransactionStatisticTrendsItemSlice, 0, len(allMonthlyTotalAmounts))
for yearMonth, monthlyTotalAmounts := range allMonthlyTotalAmounts {
monthlyStatisticResp := &models.TransactionStatisticTrendsItem{
Year: yearMonth / 100,
Month: yearMonth % 100,
Items: make([]*models.TransactionStatisticResponseItem, len(monthlyTotalAmounts)),
}
for i := 0; i < len(monthlyTotalAmounts); i++ {
totalAmountItem := monthlyTotalAmounts[i]
monthlyStatisticResp.Items[i] = &models.TransactionStatisticResponseItem{
CategoryId: totalAmountItem.CategoryId,
AccountId: totalAmountItem.AccountId,
TotalAmount: totalAmountItem.Amount,
}
}
statisticTrendsResp = append(statisticTrendsResp, monthlyStatisticResp)
}
sort.Sort(statisticTrendsResp)
return statisticTrendsResp, nil
}
// TransactionAmountsHandler returns transaction amounts of current user
func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs.Error) {
var transactionAmountsReq models.TransactionAmountsRequest
@@ -295,6 +396,13 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs
return nil, errs.ErrQueryItemsTooMuch
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
@@ -310,7 +418,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs
for i := 0; i < len(requestItems); i++ {
requestItem := requestItems[i]
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime)
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, utcOffset, transactionAmountsReq.UseTransactionTimezone)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
@@ -381,120 +489,6 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs
return amountsResp, nil
}
// TransactionMonthAmountsHandler returns every month transaction amounts of current user
func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (any, *errs.Error) {
var transactionAmountsReq models.TransactionMonthAmountsRequest
err := c.ShouldBindQuery(&transactionAmountsReq)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
startTime, endTime, err := transactionAmountsReq.GetStartTimeAndEndTime(utcOffset)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] parse request start or end date failed, because %s", err.Error())
return nil, errs.ErrParameterInvalid
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
accountMap := a.accounts.GetAccountMapByList(accounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
totalAmounts, err := a.transactions.GetAccountsMonthTotalIncomeAndExpense(c, uid, startTime, endTime, pageCountForLoadTransactionAmounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get accounts month total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsMap := make(map[string]map[string]*models.TransactionAmountsResponseItemAmountInfo)
for yearMonth, monthAccountsAmounts := range totalAmounts {
for accountId, monthAccountAmounts := range monthAccountsAmounts {
account, exists := accountMap[accountId]
if !exists {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot find account for account \"id:%d\" of user \"uid:%d\"", accountId, uid)
continue
}
monthTotalAmounts, exists := amountsMap[yearMonth]
if !exists {
monthTotalAmounts = make(map[string]*models.TransactionAmountsResponseItemAmountInfo)
amountsMap[yearMonth] = monthTotalAmounts
}
monthTotalAmount, exists := monthTotalAmounts[account.Currency]
if !exists {
monthTotalAmount = &models.TransactionAmountsResponseItemAmountInfo{
Currency: account.Currency,
IncomeAmount: 0,
ExpenseAmount: 0,
}
}
monthTotalAmount.IncomeAmount += monthAccountAmounts.TotalIncomeAmount
monthTotalAmount.ExpenseAmount += monthAccountAmounts.TotalExpenseAmount
monthTotalAmounts[account.Currency] = monthTotalAmount
}
}
amountsResp := make(models.TransactionMonthAmountsResponseItemSlice, 0)
for yearMonth, monthTotalAmounts := range amountsMap {
yearMonthItems := strings.Split(yearMonth, "-")
year, err := utils.StringToInt32(yearMonthItems[0])
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get year from year-month item \"%s\" for user \"uid:%d\"", yearMonth, uid)
continue
}
month, err := utils.StringToInt32(yearMonthItems[1])
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get month from year-month item \"%s\" for user \"uid:%d\"", yearMonth, uid)
continue
}
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,
Amounts: amounts,
})
}
sort.Sort(amountsResp)
return amountsResp, nil
}
// TransactionGetHandler returns one specific transaction of current user
func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (any, *errs.Error) {
var transactionGetReq models.TransactionGetRequest
@@ -669,6 +663,28 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs.
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
}
if settings.Container.Current.EnableDuplicateSubmissionsCheck && transactionCreateReq.ClientSessionId != "" {
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId)
if found {
log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] another transaction \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
transactionId, err := utils.StringToInt64(remark)
if err == nil {
transaction, err = a.transactions.GetTransactionByTransactionId(c, uid, transactionId)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionCreateHandler] failed to get existed transaction \"id:%d\" for user \"uid:%d\", because %s", transactionId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
return transactionResp, nil
}
}
}
err = a.transactions.CreateTransaction(c, transaction, tagIds)
if err != nil {
@@ -678,6 +694,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs.
log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId)
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId, utils.Int64ToString(transaction.TransactionId))
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
return transactionResp, nil
@@ -788,7 +805,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (any, *errs.
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
}
err = a.transactions.ModifyTransaction(c, newTransaction, addTransactionTagIds, removeTransactionTagIds)
err = a.transactions.ModifyTransaction(c, newTransaction, len(transactionTagIds), 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())
@@ -884,50 +901,124 @@ func (a *TransactionsApi) filterTransactions(c *core.Context, uid int64, transac
return finalTransactions
}
func (a *TransactionsApi) getAccountOrSubAccountIds(c *core.Context, accountId int64, uid int64) ([]int64, error) {
func (a *TransactionsApi) getAccountOrSubAccountIds(c *core.Context, accountIds string, uid int64) ([]int64, error) {
if accountIds == "" || accountIds == "0" {
return nil, nil
}
requestAccountIds, err := utils.StringArrayToInt64Array(strings.Split(accountIds, ","))
if err != nil {
return nil, errs.Or(err, errs.ErrAccountIdInvalid)
}
var allAccountIds []int64
if accountId > 0 {
allSubAccounts, err := a.accounts.GetSubAccountsByAccountId(c, uid, accountId)
if len(requestAccountIds) > 0 {
allSubAccounts, err := a.accounts.GetSubAccountsByAccountIds(c, uid, requestAccountIds)
if err != nil {
return nil, err
}
if len(allSubAccounts) > 0 {
for i := 0; i < len(allSubAccounts); i++ {
allAccountIds = append(allAccountIds, allSubAccounts[i].AccountId)
accountIdsMap := make(map[int64]int32, len(requestAccountIds))
for i := 0; i < len(requestAccountIds); i++ {
accountIdsMap[requestAccountIds[i]] = 0
}
for i := 0; i < len(allSubAccounts); i++ {
subAccount := allSubAccounts[i]
if refCount, exists := accountIdsMap[subAccount.ParentAccountId]; exists {
accountIdsMap[subAccount.ParentAccountId] = refCount + 1
} else {
accountIdsMap[subAccount.ParentAccountId] = 1
}
if _, exists := accountIdsMap[subAccount.AccountId]; exists {
delete(accountIdsMap, subAccount.AccountId)
}
allAccountIds = append(allAccountIds, subAccount.AccountId)
}
for accountId, refCount := range accountIdsMap {
if refCount < 1 {
allAccountIds = append(allAccountIds, accountId)
}
} else {
allAccountIds = append(allAccountIds, accountId)
}
}
return allAccountIds, nil
}
func (a *TransactionsApi) getCategoryOrSubCategoryIds(c *core.Context, categoryId int64, uid int64) ([]int64, error) {
func (a *TransactionsApi) getCategoryOrSubCategoryIds(c *core.Context, categoryIds string, uid int64) ([]int64, error) {
if categoryIds == "" || categoryIds == "0" {
return nil, nil
}
requestCategoryIds, err := utils.StringArrayToInt64Array(strings.Split(categoryIds, ","))
if err != nil {
return nil, errs.Or(err, errs.ErrTransactionCategoryIdInvalid)
}
var allCategoryIds []int64
if categoryId > 0 {
allSubCategories, err := a.transactionCategories.GetAllCategoriesByUid(c, uid, 0, categoryId)
if len(requestCategoryIds) > 0 {
allSubCategories, err := a.transactionCategories.GetSubCategoriesByCategoryIds(c, uid, requestCategoryIds)
if err != nil {
return nil, err
}
if len(allSubCategories) > 0 {
for i := 0; i < len(allSubCategories); i++ {
allCategoryIds = append(allCategoryIds, allSubCategories[i].CategoryId)
categoryIdsMap := make(map[int64]int32, len(requestCategoryIds))
for i := 0; i < len(requestCategoryIds); i++ {
categoryIdsMap[requestCategoryIds[i]] = 0
}
for i := 0; i < len(allSubCategories); i++ {
subCategory := allSubCategories[i]
if refCount, exists := categoryIdsMap[subCategory.ParentCategoryId]; exists {
categoryIdsMap[subCategory.ParentCategoryId] = refCount + 1
} else {
categoryIdsMap[subCategory.ParentCategoryId] = 1
}
if _, exists := categoryIdsMap[subCategory.CategoryId]; exists {
delete(categoryIdsMap, subCategory.CategoryId)
}
allCategoryIds = append(allCategoryIds, subCategory.CategoryId)
}
for accountId, refCount := range categoryIdsMap {
if refCount < 1 {
allCategoryIds = append(allCategoryIds, accountId)
}
} else {
allCategoryIds = append(allCategoryIds, categoryId)
}
}
return allCategoryIds, nil
}
func (a *TransactionsApi) getTagIds(tagIds string) ([]int64, error) {
if tagIds == "" || tagIds == "0" {
return nil, nil
}
requestTagIds, err := utils.StringArrayToInt64Array(strings.Split(tagIds, ","))
if err != nil {
return nil, errs.Or(err, errs.ErrTransactionTagIdInvalid)
}
return requestTagIds, nil
}
func (a *TransactionsApi) getTransactionTagIds(allTransactionTagIds map[int64][]int64) []int64 {
allTagIds := make([]int64, 0, len(allTransactionTagIds))
+17 -17
View File
@@ -45,7 +45,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (an
}
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -63,7 +63,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
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())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -84,14 +84,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
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())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor secret, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
img, err := key.Image(240, 240)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor qrcode, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor qrcode, because %s", err.Error())
return nil, errs.ErrOperationFailed
}
@@ -123,7 +123,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
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())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -154,25 +154,25 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
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())
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(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())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two factor authorization", uid)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two-factor authorization", uid)
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
@@ -236,7 +236,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (a
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())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -247,18 +247,18 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (a
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)
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(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor setting for user \"uid:%d\"", uid)
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor setting for user \"uid:%d\"", uid)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two factor authorization", uid)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two-factor authorization", uid)
return true, nil
}
@@ -291,7 +291,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
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())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -302,14 +302,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
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())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -317,7 +317,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
RecoveryCodes: recoveryCodes,
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two factor recovery codes", uid)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two-factor recovery codes", uid)
return recoveryCodesResp, nil
}
+276 -4
View File
@@ -1,6 +1,8 @@
package api
import (
"io"
"os"
"strings"
"time"
@@ -8,10 +10,13 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
"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/storage"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
@@ -86,8 +91,9 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
authResp := &models.RegisterResponse{
AuthResponse: models.AuthResponse{
Need2FA: false,
User: user.ToUserBasicInfo(),
Need2FA: false,
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterRegisterNotificationContent(user.Language, c.GetClientLocale()),
},
NeedVerifyEmail: settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableUserForceVerifyEmail,
PresetCategoriesSaved: presetCategoriesSaved,
@@ -182,6 +188,8 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
resp.NewToken = token
resp.User = user.ToUserBasicInfo()
resp.NotificationContent = settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale())
c.SetTextualToken(token)
c.SetTokenClaims(claims)
@@ -262,12 +270,22 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
}
if userUpdateReq.DefaultAccountId > 0 && userUpdateReq.DefaultAccountId != user.DefaultAccountId {
accounts, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
if err != nil || len(accounts) < 1 {
if err != nil || len(accountMap) < 1 {
return nil, errs.Or(err, errs.ErrUserDefaultAccountIsInvalid)
}
if _, exists := accountMap[userUpdateReq.DefaultAccountId]; !exists {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] account \"id:%d\" does not exist for user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
return nil, errs.ErrUserDefaultAccountIsInvalid
}
if accountMap[userUpdateReq.DefaultAccountId].Hidden {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] account \"id:%d\" is hidden of user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
return nil, errs.ErrUserDefaultAccountIsHidden
}
user.DefaultAccountId = userUpdateReq.DefaultAccountId
userNew.DefaultAccountId = userUpdateReq.DefaultAccountId
anythingUpdate = true
@@ -336,6 +354,81 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
userNew.ShortTimeFormat = models.SHORT_TIME_FORMAT_INVALID
}
if userUpdateReq.DecimalSeparator != nil && *userUpdateReq.DecimalSeparator != user.DecimalSeparator {
user.DecimalSeparator = *userUpdateReq.DecimalSeparator
userNew.DecimalSeparator = *userUpdateReq.DecimalSeparator
anythingUpdate = true
} else {
userNew.DecimalSeparator = core.DECIMAL_SEPARATOR_INVALID
}
if userUpdateReq.DigitGroupingSymbol != nil && *userUpdateReq.DigitGroupingSymbol != user.DigitGroupingSymbol {
user.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
userNew.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
anythingUpdate = true
} else {
userNew.DigitGroupingSymbol = core.DIGIT_GROUPING_SYMBOL_INVALID
}
if userUpdateReq.DigitGrouping != nil && *userUpdateReq.DigitGrouping != user.DigitGrouping {
user.DigitGrouping = *userUpdateReq.DigitGrouping
userNew.DigitGrouping = *userUpdateReq.DigitGrouping
anythingUpdate = true
} else {
userNew.DigitGrouping = core.DIGIT_GROUPING_TYPE_INVALID
}
if userUpdateReq.CurrencyDisplayType != nil && *userUpdateReq.CurrencyDisplayType != user.CurrencyDisplayType {
user.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
userNew.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
anythingUpdate = true
} else {
userNew.CurrencyDisplayType = models.CURRENCY_DISPLAY_TYPE_INVALID
}
if userUpdateReq.ExpenseAmountColor != nil && *userUpdateReq.ExpenseAmountColor != user.ExpenseAmountColor {
user.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
userNew.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
anythingUpdate = true
} else {
userNew.ExpenseAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
}
if userUpdateReq.IncomeAmountColor != nil && *userUpdateReq.IncomeAmountColor != user.IncomeAmountColor {
user.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
userNew.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
anythingUpdate = true
} else {
userNew.IncomeAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
}
if modifyUserLanguage || userNew.DecimalSeparator != core.DECIMAL_SEPARATOR_INVALID || userNew.DigitGroupingSymbol != core.DIGIT_GROUPING_SYMBOL_INVALID {
decimalSeparator := userNew.DecimalSeparator
digitGroupingSymbol := userNew.DigitGroupingSymbol
if userNew.DecimalSeparator == core.DECIMAL_SEPARATOR_INVALID {
decimalSeparator = user.DecimalSeparator
}
if userNew.DigitGroupingSymbol == core.DIGIT_GROUPING_SYMBOL_INVALID {
digitGroupingSymbol = user.DigitGroupingSymbol
}
locale := user.Language
if modifyUserLanguage {
locale = userNew.Language
}
if locale == "" {
locale = c.GetClientLocale()
}
if locales.IsDecimalSeparatorEqualsDigitGroupingSymbol(decimalSeparator, digitGroupingSymbol, locale) {
return nil, errs.ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual
}
}
if !anythingUpdate {
return nil, errs.ErrNothingWillBeUpdated
}
@@ -408,6 +501,128 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
return resp, nil
}
// UserUpdateAvatarHandler saves user avatar by request parameters for current user
func (a *UsersApi) UserUpdateAvatarHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
form, err := c.MultipartForm()
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get multi-part form data for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrParameterInvalid
}
avatars := form.File["avatar"]
if len(avatars) < 1 {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] there is no user avatar in request for user \"uid:%d\"", user.Uid)
return nil, errs.ErrNoUserAvatar
}
if avatars[0].Size < 1 {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the size of user avatar in request is zero for user \"uid:%d\"", user.Uid)
return nil, errs.ErrUserAvatarIsEmpty
}
fileExtension := utils.GetFileNameExtension(avatars[0].Filename)
if utils.GetImageContentType(fileExtension) == "" {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the file extension \"%s\" of user avatar in request is not supported for user \"uid:%d\"", fileExtension, user.Uid)
return nil, errs.ErrImageTypeNotSupported
}
avatarFile, err := avatars[0].Open()
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get avatar file from request for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrOperationFailed
}
defer avatarFile.Close()
err = storage.Container.SaveAvatar(user.Uid, avatarFile, fileExtension)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to save avatar file for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrOperationFailed
}
err = a.users.UpdateUserAvatar(c, user.Uid, fileExtension)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if fileExtension != user.CustomAvatarType {
err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to delete old avatar with extension \"%s\" for user \"uid:%d\", because %s", user.CustomAvatarType, user.Uid, err.Error())
}
}
user.CustomAvatarType = fileExtension
userResp := user.ToUserProfileResponse()
return userResp, nil
}
// UserRemoveAvatarHandler removes user avatar by request parameters for current user
func (a *UsersApi) UserRemoveAvatarHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.CustomAvatarType == "" {
return nil, errs.ErrNothingWillBeUpdated
}
err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete avatar file for user \"uid:%d\", because %s", user.Uid, err.Error())
exists, err := storage.Container.ExistsAvatar(user.Uid, user.CustomAvatarType)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to check whether avatar file exist for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrOperationFailed
}
if exists {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete whether avatar file exist for user \"uid:%d\", the avatar file still exist", user.Uid)
return nil, errs.ErrOperationFailed
}
}
err = a.users.UpdateUserAvatar(c, user.Uid, "")
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
user.CustomAvatarType = ""
userResp := user.ToUserProfileResponse()
return userResp, nil
}
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
if !settings.Container.Current.EnableUserVerifyEmail {
@@ -507,3 +722,60 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any
return true, nil
}
// UserGetAvatarHandler returns user avatar data for current user
func (a *UsersApi) UserGetAvatarHandler(c *core.Context) ([]byte, string, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user, because %s", err.Error())
}
return nil, "", errs.ErrUserNotFound
}
if user.CustomAvatarType == "" {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user does not have avatar for user \"uid:%d\"", user.Uid)
return nil, "", errs.ErrUserAvatarNoExists
}
fileName := c.Param("fileName")
fileBaseName := utils.GetFileNameWithoutExtension(fileName)
if utils.Int64ToString(user.Uid) != fileBaseName {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] cannot get other user avatar \"uid:%s\" for user \"uid:%d\"", fileBaseName, user.Uid)
return nil, "", errs.ErrUserIdInvalid
}
fileExtension := utils.GetFileNameExtension(fileName)
if user.CustomAvatarType != fileExtension {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar extension is invalid \"%s\" for user \"uid:%d\"", fileExtension, user.Uid)
return nil, "", errs.ErrUserAvatarNoExists
}
avatarFile, err := storage.Container.ReadAvatar(user.Uid, fileExtension)
if os.IsNotExist(err) {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar file not exist for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, "", errs.ErrUserAvatarNoExists
}
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user avatar object for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
defer avatarFile.Close()
avatarData, err := io.ReadAll(avatarFile)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to read user avatar object data for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
return avatarData, utils.GetImageContentType(fileExtension), nil
}
+113 -26
View File
@@ -20,6 +20,7 @@ const pageCountForDataExport = 1000
// UserDataCli represents user data cli
type UserDataCli struct {
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
@@ -34,6 +35,7 @@ type UserDataCli struct {
var (
UserData = &UserDataCli{
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
@@ -391,7 +393,7 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
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())
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to check two-factor setting, because %s", err.Error())
return err
}
@@ -402,14 +404,14 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
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)
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two-factor recovery codes for user \"%s\"", username)
return err
}
err = l.twoFactorAuthorizations.DeleteTwoFactorSetting(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two factor setting for user \"%s\"", username)
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two-factor setting for user \"%s\"", username)
return err
}
@@ -430,7 +432,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
return false, err
}
accountMap, categoryMap, tagMap, tagIndexs, err := l.getUserEssentialData(uid, username)
accountMap, categoryMap, tagMap, tagIndexes, tagIndexesMap, err := l.getUserEssentialData(uid, username)
if err != nil {
log.BootErrorf("[user_data.CheckTransactionAndAccount] failed to get essential data for user \"%s\", because %s", username, err.Error())
@@ -470,7 +472,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
return false, err
}
err = l.checkTransactionTag(c, transaction.TransactionId, tagIndexs, tagMap)
err = l.checkTransactionTag(c, transaction.TransactionId, tagIndexesMap, tagMap)
if err != nil {
return false, err
@@ -499,7 +501,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
balance = balance + transaction.Amount
} else {
log.BootErrorf("[user_data.CheckAccountBalance] transaction type of transaction \"id:%d\" is invalid", transaction.TransactionId)
log.BootErrorf("[user_data.CheckTransactionAndAccount] transaction type of transaction \"id:%d\" is invalid", transaction.TransactionId)
return false, errs.ErrOperationFailed
}
@@ -514,12 +516,12 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
}
if !exists && account.Balance != 0 {
log.BootErrorf("[user_data.CheckAccountBalance] account \"id:%d\" balance is not correct, expected balance is %d, but there is no transaction actually", account.AccountId, account.Balance)
log.BootErrorf("[user_data.CheckTransactionAndAccount] account \"id:%d\" balance is not correct, expected balance is %d, but there is no transaction actually", account.AccountId, account.Balance)
return false, errs.ErrOperationFailed
}
if account.Balance != actualBalance {
log.BootErrorf("[user_data.CheckAccountBalance] account \"id:%d\" balance is not correct, expected balance is %d, but actual balance is %d", account.AccountId, account.Balance, actualBalance)
log.BootErrorf("[user_data.CheckTransactionAndAccount] account \"id:%d\" balance is not correct, expected balance is %d, but actual balance is %d", account.AccountId, account.Balance, actualBalance)
return false, errs.ErrOperationFailed
}
}
@@ -528,7 +530,16 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
_, exists := accountMap[accountId]
if !exists {
log.BootErrorf("[user_data.CheckAccountBalance] account \"id:%d\" does not exist, but there are some transactions of this account actually, and actual balance is %d", accountId, actualBalance)
log.BootErrorf("[user_data.CheckTransactionAndAccount] account \"id:%d\" does not exist, but there are some transactions of this account actually, and actual balance is %d", accountId, actualBalance)
return false, errs.ErrOperationFailed
}
}
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
if tagIndex.TransactionTime < 1 {
log.BootErrorf("[user_data.CheckTransactionAndAccount] transaction tag index \"id:%d\" does not have transaction time", tagIndex.TagIndexId)
return false, errs.ErrOperationFailed
}
}
@@ -536,8 +547,74 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
return true, nil
}
// FixTransactionTagIndexWithTransactionTime fixes user transaction tag index data with transaction time
func (l *UserDataCli) FixTransactionTagIndexWithTransactionTime(c *cli.Context, username string) (bool, error) {
if username == "" {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] user name is empty")
return false, errs.ErrUsernameIsEmpty
}
uid, err := l.getUserIdByUsername(c, username)
if err != nil {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] error occurs when getting user id by user name")
return false, err
}
tagIndexes, err := l.tags.GetAllTagIdsOfAllTransactions(nil, uid)
if err != nil {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] failed to get tag index for user \"%s\", because %s", username, err.Error())
return false, err
}
invalidTagIndexes := make([]*models.TransactionTagIndex, 0, len(tagIndexes))
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
if tagIndex.TransactionTime < 1 {
invalidTagIndexes = append(invalidTagIndexes, tagIndex)
}
}
if len(invalidTagIndexes) < 1 {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] all user transaction tag index data has been checked, there is no problem with user data")
return false, errs.ErrOperationFailed
}
allTransactions, err := l.transactions.GetAllTransactions(nil, uid, pageCountForGettingTransactions, false)
if err != nil {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] failed to all transactions for user \"%s\", because %s", username, err.Error())
return false, err
}
transactionMap := l.transactions.GetTransactionMapByList(allTransactions)
for i := 0; i < len(invalidTagIndexes); i++ {
tagIndex := invalidTagIndexes[i]
transaction, exists := transactionMap[tagIndex.TransactionId]
if !exists {
continue
}
tagIndex.TransactionTime = transaction.TransactionTime
}
err = l.tags.ModifyTagIndexTransactionTime(nil, uid, invalidTagIndexes)
if err != nil {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] failed to update transaction tag index for user \"%s\", because %s", username, err.Error())
return false, err
}
return true, nil
}
// ExportTransaction returns csv file content according user all transactions
func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte, error) {
func (l *UserDataCli) ExportTransaction(c *cli.Context, username string, fileType string) ([]byte, error) {
if username == "" {
log.BootErrorf("[user_data.ExportTransaction] user name is empty")
return nil, errs.ErrUsernameIsEmpty
@@ -550,7 +627,7 @@ func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte
return nil, err
}
accountMap, categoryMap, tagMap, tagIndexs, err := l.getUserEssentialData(uid, username)
accountMap, categoryMap, tagMap, _, tagIndexesMap, err := l.getUserEssentialData(uid, username)
if err != nil {
log.BootErrorf("[user_data.ExportTransaction] failed to get essential data for user \"%s\", because %s", username, err.Error())
@@ -564,7 +641,15 @@ func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte
return nil, err
}
result, err := l.ezBookKeepingCsvExporter.ToExportedContent(uid, time.Local, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
var dataExporter converters.DataConverter
if fileType == "tsv" {
dataExporter = l.ezBookKeepingTsvExporter
} else {
dataExporter = l.ezBookKeepingCsvExporter
}
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexesMap)
if err != nil {
log.BootErrorf("[user_data.ExportTransaction] failed to get csv format exported data for \"%s\", because %s", username, err.Error())
@@ -585,17 +670,17 @@ func (l *UserDataCli) getUserIdByUsername(c *cli.Context, username string) (int6
return user.Uid, nil
}
func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, tagIndexs map[int64][]int64, err error) {
func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, tagIndexes []*models.TransactionTagIndex, tagIndexesMap map[int64][]int64, err error) {
if uid <= 0 {
log.BootErrorf("[user_data.getUserEssentialData] user uid \"%d\" is invalid", uid)
return nil, nil, nil, nil, errs.ErrUserIdInvalid
return nil, nil, nil, nil, nil, errs.ErrUserIdInvalid
}
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())
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
accountMap = l.accounts.GetAccountMapByList(accounts)
@@ -604,7 +689,7 @@ func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountM
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get categories for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
categoryMap = l.categories.GetCategoryMapByList(categories)
@@ -613,19 +698,21 @@ func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountM
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get tags for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
tagMap = l.tags.GetTagMapByList(tags)
tagIndexs, err = l.tags.GetAllTagIdsOfAllTransactions(nil, uid)
tagIndexes, 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())
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
return accountMap, categoryMap, tagMap, tagIndexs, nil
tagIndexesMap = l.tags.GetGroupedTransactionTagIds(tagIndexes)
return accountMap, categoryMap, tagMap, tagIndexes, tagIndexesMap, nil
}
func (l *UserDataCli) checkTransactionAccount(c *cli.Context, transaction *models.Transaction, accountMap map[int64]*models.Account, accountHasChild map[int64]bool) error {
@@ -637,7 +724,7 @@ func (l *UserDataCli) checkTransactionAccount(c *cli.Context, transaction *model
}
if account.ParentAccountId == models.LevelOneAccountParentId && accountHasChild[account.AccountId] {
log.BootErrorf("[user_data.checkTransactionAccount] the account \"id:%d\" of transaction \"id:%d\" is not a sub account", transaction.AccountId, transaction.TransactionId)
log.BootErrorf("[user_data.checkTransactionAccount] the account \"id:%d\" of transaction \"id:%d\" is not a sub-account", transaction.AccountId, transaction.TransactionId)
return errs.ErrOperationFailed
}
@@ -650,7 +737,7 @@ func (l *UserDataCli) checkTransactionAccount(c *cli.Context, transaction *model
}
if relatedAccount.ParentAccountId == models.LevelOneAccountParentId && accountHasChild[relatedAccount.AccountId] {
log.BootErrorf("[user_data.checkTransactionAccount] the related account \"id:%d\" of transaction \"id:%d\" is not a sub account", transaction.RelatedAccountId, transaction.TransactionId)
log.BootErrorf("[user_data.checkTransactionAccount] the related account \"id:%d\" of transaction \"id:%d\" is not a sub-account", transaction.RelatedAccountId, transaction.TransactionId)
return errs.ErrOperationFailed
}
}
@@ -683,15 +770,15 @@ func (l *UserDataCli) checkTransactionCategory(c *cli.Context, transaction *mode
return nil
}
func (l *UserDataCli) checkTransactionTag(c *cli.Context, transactionId int64, allTagIndexs map[int64][]int64, tagMap map[int64]*models.TransactionTag) error {
tagIndexs, exists := allTagIndexs[transactionId]
func (l *UserDataCli) checkTransactionTag(c *cli.Context, transactionId int64, allTagIndexesMap map[int64][]int64, tagMap map[int64]*models.TransactionTag) error {
tagIndexes, exists := allTagIndexesMap[transactionId]
if !exists {
return nil
}
for i := 0; i < len(tagIndexs); i++ {
tagIndex := tagIndexs[i]
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
tag, exists := tagMap[tagIndex]
if !exists {
+1 -3
View File
@@ -1,13 +1,11 @@
package converters
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// DataConverter defines the structure of data exporter
type DataConverter interface {
// ToExportedContent returns the exported data
ToExportedContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error)
ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error)
}
+6 -168
View File
@@ -1,179 +1,17 @@
package converters
import (
"fmt"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// EzBookKeepingCSVFileExporter defines the structure of csv file exporter
// EzBookKeepingCSVFileExporter defines the structure of CSV file exporter
type EzBookKeepingCSVFileExporter struct {
DataConverter
EzBookKeepingPlainFileExporter
}
const csvHeaderLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Tags,Comment\n"
const csvDataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"
const csvSeparator = ","
// ToExportedContent returns the exported csv data
func (e *EzBookKeepingCSVFileExporter) ToExportedContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error) {
var ret strings.Builder
ret.Grow(len(transactions) * 100)
ret.WriteString(csvHeaderLine)
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
continue
}
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
transactionTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone)
transactionType := e.getTransactionTypeName(transaction.Type)
category := e.getTransactionCategoryName(transaction.CategoryId, categoryMap)
subCategory := e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap)
account := e.getAccountName(transaction.AccountId, accountMap)
accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap)
amount := e.getDisplayAmount(transaction.Amount)
account2 := ""
account2Currency := ""
account2Amount := ""
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2 = e.getAccountName(transaction.RelatedAccountId, accountMap)
account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap)
account2Amount = e.getDisplayAmount(transaction.RelatedAccountAmount)
}
tags := e.getTags(transaction.TransactionId, allTagIndexs, tagMap)
comment := e.getComment(transaction.Comment)
ret.WriteString(fmt.Sprintf(csvDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, tags, comment))
}
return []byte(ret.String()), nil
}
func (e *EzBookKeepingCSVFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
return "Balance Modification"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
return "Income"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
return "Expense"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
return "Transfer"
} else {
return ""
}
}
func (e *EzBookKeepingCSVFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if !exists {
return ""
}
if category.ParentCategoryId == 0 {
return category.Name
}
parentCategory, exists := categoryMap[category.ParentCategoryId]
if !exists {
return ""
}
return parentCategory.Name
}
func (e *EzBookKeepingCSVFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if exists {
return category.Name
} else {
return ""
}
}
func (e *EzBookKeepingCSVFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Name
} else {
return ""
}
}
func (e *EzBookKeepingCSVFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Currency
} else {
return ""
}
}
func (e *EzBookKeepingCSVFileExporter) getDisplayAmount(amount int64) string {
displayAmount := utils.Int64ToString(amount)
integer := utils.SubString(displayAmount, 0, len(displayAmount)-2)
decimals := utils.SubString(displayAmount, -2, 2)
if integer == "" {
integer = "0"
} else if integer == "-" {
integer = "-0"
}
if len(decimals) == 0 {
decimals = "00"
} else if len(decimals) == 1 {
decimals = "0" + decimals
}
return integer + "." + decimals
}
func (e *EzBookKeepingCSVFileExporter) getTags(transactionId int64, allTagIndexs map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
tagIndexs, exists := allTagIndexs[transactionId]
if !exists {
return ""
}
var ret strings.Builder
for i := 0; i < len(tagIndexs); i++ {
if i > 0 {
ret.WriteString(";")
}
tagIndex := tagIndexs[i]
tag, exists := tagMap[tagIndex]
if !exists {
continue
}
ret.WriteString(tag.Name)
}
return ret.String()
}
func (e *EzBookKeepingCSVFileExporter) getComment(comment string) string {
comment = strings.Replace(comment, ",", " ", -1)
comment = strings.Replace(comment, "\r\n", " ", -1)
comment = strings.Replace(comment, "\n", " ", -1)
return comment
// ToExportedContent returns the exported CSV data
func (e *EzBookKeepingCSVFileExporter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
return e.toExportedContent(uid, csvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
}
+195
View File
@@ -0,0 +1,195 @@
package converters
import (
"fmt"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// EzBookKeepingPlainFileExporter defines the structure of plain file exporter
type EzBookKeepingPlainFileExporter struct {
}
const lineSeparator = "\n"
const geoLocationSeparator = " "
const transactionTagSeparator = ";"
const headerLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description" + lineSeparator
const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" + lineSeparator
// toExportedContent returns the exported plain data
func (e *EzBookKeepingPlainFileExporter) toExportedContent(uid int64, separator string, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
var ret strings.Builder
ret.Grow(len(transactions) * 100)
actualHeaderLine := headerLine
actualDataLineFormat := dataLineFormat
if separator != "," {
actualHeaderLine = strings.Replace(headerLine, ",", separator, -1)
actualDataLineFormat = strings.Replace(dataLineFormat, ",", separator, -1)
}
ret.WriteString(actualHeaderLine)
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
continue
}
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
transactionTime := utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone)
transactionType := e.getTransactionTypeName(transaction.Type)
category := e.replaceDelimiters(e.getTransactionCategoryName(transaction.CategoryId, categoryMap), separator)
subCategory := e.replaceDelimiters(e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap), separator)
account := e.replaceDelimiters(e.getAccountName(transaction.AccountId, accountMap), separator)
accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap)
amount := e.getDisplayAmount(transaction.Amount)
account2 := ""
account2Currency := ""
account2Amount := ""
geoLocation := ""
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2 = e.replaceDelimiters(e.getAccountName(transaction.RelatedAccountId, accountMap), separator)
account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap)
account2Amount = e.getDisplayAmount(transaction.RelatedAccountAmount)
}
if transaction.GeoLongitude != 0 || transaction.GeoLatitude != 0 {
geoLocation = fmt.Sprintf("%f%s%f", transaction.GeoLongitude, geoLocationSeparator, transaction.GeoLatitude)
}
tags := e.replaceDelimiters(e.getTags(transaction.TransactionId, allTagIndexes, tagMap), separator)
comment := e.replaceDelimiters(transaction.Comment, separator)
ret.WriteString(fmt.Sprintf(actualDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, geoLocation, tags, comment))
}
return []byte(ret.String()), nil
}
func (e *EzBookKeepingPlainFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
return "Balance Modification"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
return "Income"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
return "Expense"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
return "Transfer"
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if !exists {
return ""
}
if category.ParentCategoryId == 0 {
return category.Name
}
parentCategory, exists := categoryMap[category.ParentCategoryId]
if !exists {
return ""
}
return parentCategory.Name
}
func (e *EzBookKeepingPlainFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if exists {
return category.Name
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Name
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Currency
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileExporter) getDisplayAmount(amount int64) string {
displayAmount := utils.Int64ToString(amount)
integer := utils.SubString(displayAmount, 0, len(displayAmount)-2)
decimals := utils.SubString(displayAmount, -2, 2)
if integer == "" {
integer = "0"
} else if integer == "-" {
integer = "-0"
}
if len(decimals) == 0 {
decimals = "00"
} else if len(decimals) == 1 {
decimals = "0" + decimals
}
return integer + "." + decimals
}
func (e *EzBookKeepingPlainFileExporter) getTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
tagIndexes, exists := allTagIndexes[transactionId]
if !exists {
return ""
}
var ret strings.Builder
for i := 0; i < len(tagIndexes); i++ {
if i > 0 {
ret.WriteString(transactionTagSeparator)
}
tagIndex := tagIndexes[i]
tag, exists := tagMap[tagIndex]
if !exists {
continue
}
ret.WriteString(tag.Name)
}
return ret.String()
}
func (e *EzBookKeepingPlainFileExporter) replaceDelimiters(text string, separator string) string {
text = strings.Replace(text, separator, " ", -1)
text = strings.Replace(text, "\r\n", " ", -1)
text = strings.Replace(text, "\n", " ", -1)
return text
}
+17
View File
@@ -0,0 +1,17 @@
package converters
import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// EzBookKeepingTSVFileExporter defines the structure of TSV file exporter
type EzBookKeepingTSVFileExporter struct {
EzBookKeepingPlainFileExporter
}
const tsvSeparator = "\t"
// ToExportedContent returns the exported TSV data
func (e *EzBookKeepingTSVFileExporter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
return e.toExportedContent(uid, tsvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
}
+34
View File
@@ -1,6 +1,7 @@
package core
import (
"net"
"strconv"
"github.com/gin-gonic/gin"
@@ -16,6 +17,9 @@ const responseErrorFieldKey = "RESPONSE_ERROR"
// AcceptLanguageHeaderName represents the header name of accept language
const AcceptLanguageHeaderName = "Accept-Language"
// RemoteClientPortHeader represents the header name of remote client source port
const RemoteClientPortHeader = "X-Real-Port"
// ClientTimezoneOffsetHeaderName represents the header name of client timezone offset
const ClientTimezoneOffsetHeaderName = "X-Timezone-Offset"
@@ -25,6 +29,36 @@ type Context struct {
// DO NOT ADD ANY FIELD IN THIS CONTEXT, THIS CONTEXT IS JUST A WRAPPER
}
func (c *Context) ClientPort() uint16 {
remotePort := c.GetHeader(RemoteClientPortHeader)
if remotePort != "" {
remotePortNum, err := strconv.ParseInt(remotePort, 10, 32)
if err == nil {
return uint16(remotePortNum)
}
}
if c.Request == nil {
return 0
}
_, remotePort, err := net.SplitHostPort(c.Request.RemoteAddr)
if err != nil {
return 0
}
remotePortNum, err := strconv.ParseInt(remotePort, 10, 32)
if err != nil {
return 0
}
return uint16(remotePortNum)
}
// SetRequestId sets the given request id to context
func (c *Context) SetRequestId(requestId string) {
c.Set(requestIdFieldKey, requestId)
+4 -1
View File
@@ -12,8 +12,11 @@ type MiddlewareHandlerFunc func(*Context)
// ApiHandlerFunc represents the api handler function
type ApiHandlerFunc func(*Context) (any, *errs.Error)
// DataHandlerFunc represents the handler function that returns byte array
// DataHandlerFunc represents the handler function that returns file data byte array and file name
type DataHandlerFunc func(*Context) ([]byte, string, *errs.Error)
// ImageHandlerFunc represents the handler function that returns image byte array and content type
type ImageHandlerFunc func(*Context) ([]byte, string, *errs.Error)
// ProxyHandlerFunc represents the reverse proxy handler function
type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
+95
View File
@@ -0,0 +1,95 @@
package core
import (
"fmt"
)
// DecimalSeparator represents the type of decimal separator
type DecimalSeparator byte
// Decimal Separator
const (
DECIMAL_SEPARATOR_DEFAULT DecimalSeparator = 0
DECIMAL_SEPARATOR_DOT DecimalSeparator = 1
DECIMAL_SEPARATOR_COMMA DecimalSeparator = 2
DECIMAL_SEPARATOR_SPACE DecimalSeparator = 3
DECIMAL_SEPARATOR_INVALID DecimalSeparator = 255
)
// String returns a textual representation of the decimal separator enum
func (f DecimalSeparator) String() string {
switch f {
case DECIMAL_SEPARATOR_DEFAULT:
return "Default"
case DECIMAL_SEPARATOR_DOT:
return "Dot"
case DECIMAL_SEPARATOR_COMMA:
return "Comma"
case DECIMAL_SEPARATOR_SPACE:
return "Space"
case DECIMAL_SEPARATOR_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// DigitGroupingSymbol represents the digit grouping symbol
type DigitGroupingSymbol byte
// Digit Grouping Symbol
const (
DIGIT_GROUPING_SYMBOL_DEFAULT DigitGroupingSymbol = 0
DIGIT_GROUPING_SYMBOL_DOT DigitGroupingSymbol = 1
DIGIT_GROUPING_SYMBOL_COMMA DigitGroupingSymbol = 2
DIGIT_GROUPING_SYMBOL_SPACE DigitGroupingSymbol = 3
DIGIT_GROUPING_SYMBOL_APOSTROPHE DigitGroupingSymbol = 4
DIGIT_GROUPING_SYMBOL_INVALID DigitGroupingSymbol = 255
)
// String returns a textual representation of the digit grouping symbol enum
func (f DigitGroupingSymbol) String() string {
switch f {
case DIGIT_GROUPING_SYMBOL_DEFAULT:
return "Default"
case DIGIT_GROUPING_SYMBOL_DOT:
return "Dot"
case DIGIT_GROUPING_SYMBOL_COMMA:
return "Comma"
case DIGIT_GROUPING_SYMBOL_SPACE:
return "Space"
case DIGIT_GROUPING_SYMBOL_APOSTROPHE:
return "Apostrophe"
case DIGIT_GROUPING_SYMBOL_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// DigitGroupingType represents digit grouping type
type DigitGroupingType byte
// Digit Grouping Type
const (
DIGIT_GROUPING_TYPE_DEFAULT DigitGroupingType = 0
DIGIT_GROUPING_TYPE_NONE DigitGroupingType = 1
DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR DigitGroupingType = 2
DIGIT_GROUPING_TYPE_INVALID DigitGroupingType = 255
)
// String returns a textual representation of the digit grouping type enum
func (d DigitGroupingType) String() string {
switch d {
case DIGIT_GROUPING_TYPE_DEFAULT:
return "Default"
case DIGIT_GROUPING_TYPE_NONE:
return "None"
case DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR:
return "Thousands Separator"
case DIGIT_GROUPING_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ func (c *XOrmContextAdapter) Err() error {
// 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 fmt.Sprintf("%s", c.requestId)
}
return nil
@@ -0,0 +1,7 @@
package duplicatechecker
// DuplicateChecker is common duplicate checker interface
type DuplicateChecker interface {
Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string)
Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string)
}
@@ -0,0 +1,38 @@
package duplicatechecker
import (
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// DuplicateCheckerContainer contains the current duplicate checker
type DuplicateCheckerContainer struct {
Current DuplicateChecker
}
// Initialize a duplicate checker container singleton instance
var (
Container = &DuplicateCheckerContainer{}
)
// InitializeDuplicateChecker initializes the current duplicate checker according to the config
func InitializeDuplicateChecker(config *settings.Config) error {
if config.DuplicateCheckerType == settings.InMemoryDuplicateCheckerType {
checker, err := NewInMemoryDuplicateChecker(config)
Container.Current = checker
return err
}
return errs.ErrInvalidDuplicateCheckerType
}
// Get returns whether the same submission has been processed and related remark by the current duplicate checker
func (c *DuplicateCheckerContainer) Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string) {
return c.Current.Get(checkerType, uid, identification)
}
// Set saves the identification and remark to in-memory cache by the current duplicate checker
func (c *DuplicateCheckerContainer) Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string) {
c.Current.Set(checkerType, uid, identification, remark)
}
@@ -0,0 +1,13 @@
package duplicatechecker
// DuplicateCheckerType represents duplicate checker type
type DuplicateCheckerType uint8
// Types of uuid
const (
DUPLICATE_CHECKER_TYPE_DEFAULT DuplicateCheckerType = 0
DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT DuplicateCheckerType = 1
DUPLICATE_CHECKER_TYPE_NEW_CATEGORY DuplicateCheckerType = 2
DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4
)
@@ -0,0 +1,43 @@
package duplicatechecker
import (
"fmt"
"github.com/patrickmn/go-cache"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// InMemoryDuplicateChecker represents in-memory duplicate checker
type InMemoryDuplicateChecker struct {
cache *cache.Cache
}
// NewInMemoryDuplicateChecker returns a new in-memory duplicate checker
func NewInMemoryDuplicateChecker(config *settings.Config) (*InMemoryDuplicateChecker, error) {
checker := &InMemoryDuplicateChecker{
cache: cache.New(config.DuplicateSubmissionsIntervalDuration, config.InMemoryDuplicateCheckerCleanupIntervalDuration),
}
return checker, nil
}
// Get returns whether the same submission has been processed and related remark
func (c *InMemoryDuplicateChecker) Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string) {
existedRemark, found := c.cache.Get(c.getCacheKey(checkerType, uid, identification))
if found {
return true, existedRemark.(string)
}
return false, ""
}
// Set saves the identification and remark to in-memory cache
func (c *InMemoryDuplicateChecker) Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string) {
c.cache.Set(c.getCacheKey(checkerType, uid, identification), remark, cache.DefaultExpiration)
}
func (c *InMemoryDuplicateChecker) getCacheKey(checkerType DuplicateCheckerType, uid int64, identification string) string {
return fmt.Sprintf("%d|%d|%s", checkerType, uid, identification)
}
+6 -5
View File
@@ -8,14 +8,15 @@ var (
ErrAccountNotFound = NewNormalError(NormalSubcategoryAccount, 1, http.StatusBadRequest, "account not found")
ErrAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 2, http.StatusBadRequest, "account type is invalid")
ErrAccountCurrencyInvalid = NewNormalError(NormalSubcategoryAccount, 3, http.StatusBadRequest, "account currency is invalid")
ErrAccountHaveNoSubAccount = NewNormalError(NormalSubcategoryAccount, 4, http.StatusBadRequest, "account must have at least one sub account")
ErrAccountCannotHaveSubAccounts = NewNormalError(NormalSubcategoryAccount, 5, http.StatusBadRequest, "account cannot have sub accounts")
ErrAccountHaveNoSubAccount = NewNormalError(NormalSubcategoryAccount, 4, http.StatusBadRequest, "account must have at least one sub-account")
ErrAccountCannotHaveSubAccounts = NewNormalError(NormalSubcategoryAccount, 5, http.StatusBadRequest, "account cannot have sub-accounts")
ErrParentAccountCannotSetCurrency = NewNormalError(NormalSubcategoryAccount, 6, http.StatusBadRequest, "parent account cannot set currency")
ErrParentAccountCannotSetBalance = NewNormalError(NormalSubcategoryAccount, 7, http.StatusBadRequest, "parent account cannot set balance")
ErrSubAccountCategoryNotEqualsToParent = NewNormalError(NormalSubcategoryAccount, 8, http.StatusBadRequest, "sub account category not equals to parent")
ErrSubAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 9, http.StatusBadRequest, "sub account type invalid")
ErrCannotAddOrDeleteSubAccountsWhenModify = NewNormalError(NormalSubcategoryAccount, 10, http.StatusBadRequest, "cannot add or delete sub accounts when modify account")
ErrSubAccountCategoryNotEqualsToParent = NewNormalError(NormalSubcategoryAccount, 8, http.StatusBadRequest, "sub-account category not equals to parent")
ErrSubAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 9, http.StatusBadRequest, "sub-account type invalid")
ErrCannotAddOrDeleteSubAccountsWhenModify = NewNormalError(NormalSubcategoryAccount, 10, http.StatusBadRequest, "cannot add or delete sub-accounts when modify account")
ErrSourceAccountNotFound = NewNormalError(NormalSubcategoryAccount, 11, http.StatusBadRequest, "source account not found")
ErrDestinationAccountNotFound = NewNormalError(NormalSubcategoryAccount, 12, http.StatusBadRequest, "destination account not found")
ErrAccountInUseCannotBeDeleted = NewNormalError(NormalSubcategoryAccount, 13, http.StatusBadRequest, "account is in use and cannot be deleted")
ErrAccountCategoryInvalid = NewNormalError(NormalSubcategoryAccount, 14, http.StatusBadRequest, "account category is invalid")
)
+12
View File
@@ -15,6 +15,7 @@ const (
SystemSubcategorySetting = 1
SystemSubcategoryDatabase = 2
SystemSubcategoryMail = 3
SystemSubcategoryLogging = 4
)
// Sub categories of normal error
@@ -28,6 +29,8 @@ const (
NormalSubcategoryCategory = 6
NormalSubcategoryTag = 7
NormalSubcategoryDataManagement = 8
NormalSubcategoryMapProxy = 9
NormalSubcategoryTemplate = 10
)
// Error represents the specific error returned to user
@@ -73,6 +76,15 @@ func NewNormalError(subCategory int32, index int32, httpStatusCode int, message
return New(CATEGORY_NORMAL, subCategory, index, httpStatusCode, message)
}
// NewLoggingError returns a new logging error instance
func NewLoggingError(message string, err ...error) *Error {
return New(ErrLoggingError.Category,
ErrLoggingError.SubCategory,
ErrLoggingError.Index,
ErrLoggingError.HttpStatusCode,
message, err...)
}
// NewIncompleteOrIncorrectSubmissionError returns a new incomplete or incorrect submission error instance
func NewIncompleteOrIncorrectSubmissionError(err error) *Error {
return New(ErrIncompleteOrIncorrectSubmission.Category,
+6 -1
View File
@@ -16,7 +16,7 @@ var (
ErrPageIndexInvalid = NewNormalError(NormalSubcategoryGlobal, 6, http.StatusBadRequest, "page index is invalid")
ErrPageCountInvalid = NewNormalError(NormalSubcategoryGlobal, 7, http.StatusBadRequest, "page count is invalid")
ErrClientTimezoneOffsetInvalid = NewNormalError(NormalSubcategoryGlobal, 8, http.StatusBadRequest, "client timezone offset is invalid")
ErrQueryItemsEmpty = NewNormalError(NormalSubcategoryGlobal, 9, http.StatusBadRequest, "query items cannot be empty")
ErrQueryItemsEmpty = NewNormalError(NormalSubcategoryGlobal, 9, http.StatusBadRequest, "query items cannot be blank")
ErrQueryItemsTooMuch = NewNormalError(NormalSubcategoryGlobal, 10, http.StatusBadRequest, "query items too much")
ErrQueryItemsInvalid = NewNormalError(NormalSubcategoryGlobal, 11, http.StatusBadRequest, "query items have invalid item")
ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid")
@@ -82,3 +82,8 @@ func GetParameterInvalidCurrencyMessage(field string) string {
func GetParameterInvalidHexRGBColorMessage(field string) string {
return fmt.Sprintf("parameter \"%s\" is invalid color", field)
}
// GetParameterInvalidAmountFilterMessage returns specific error message for invalid amount filter parameter error
func GetParameterInvalidAmountFilterMessage(field string) string {
return fmt.Sprintf("parameter \"%s\" is invalid amount filter", field)
}
+10
View File
@@ -0,0 +1,10 @@
package errs
import (
"net/http"
)
// Error codes related to logging
var (
ErrLoggingError = NewSystemError(SystemSubcategoryLogging, 0, http.StatusInternalServerError, "logging error")
)
+9
View File
@@ -0,0 +1,9 @@
package errs
import "net/http"
// Error codes related to map image proxy
var (
ErrMapProviderNotCurrent = NewNormalError(NormalSubcategoryMapProxy, 0, http.StatusBadRequest, "specified map provider is not set")
ErrImageExtensionNotSupported = NewNormalError(NormalSubcategoryMapProxy, 0, http.StatusNotFound, "specified image extension is not supported")
)
+19 -7
View File
@@ -4,11 +4,23 @@ 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")
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid amap security verification method")
ErrInvalidServerMode = NewSystemError(SystemSubcategorySetting, 0, http.StatusInternalServerError, "invalid server mode")
ErrInvalidProtocol = NewSystemError(SystemSubcategorySetting, 1, http.StatusInternalServerError, "invalid server protocol")
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "invalid log mode")
ErrInvalidLogLevel = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid log level")
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "failed to get local address")
ErrInvalidStorageType = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid storage type")
ErrInvalidLocalFileSystemStoragePath = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid local file system storage path")
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid uuid mode")
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid duplicate checker type")
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid token expired time")
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid token min refresh interval")
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid temporary token expired time")
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid email verify token expired time")
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid avatar provider")
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid map provider")
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 16, http.StatusInternalServerError, "invalid amap security verification method")
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 17, http.StatusInternalServerError, "invalid password reset token expired time")
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 18, http.StatusInternalServerError, "invalid exchange rates data source")
)
+7 -5
View File
@@ -4,9 +4,11 @@ import "net/http"
// Error codes related to transaction categories
var (
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
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")
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
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.StatusServiceUnavailable, "system is busy")
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
)
+2 -2
View File
@@ -11,8 +11,8 @@ var (
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")
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")
+9 -3
View File
@@ -18,7 +18,13 @@ var (
ErrCannotAddTransactionToHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 11, http.StatusBadRequest, "cannot add transaction to hidden account")
ErrCannotModifyTransactionInHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 12, http.StatusBadRequest, "cannot modify transaction of hidden account")
ErrCannotDeleteTransactionInHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 13, http.StatusBadRequest, "cannot delete transaction in hidden account")
ErrCannotCreateTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 14, http.StatusBadRequest, "cannot add transaction with this transaction time")
ErrCannotModifyTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 15, http.StatusBadRequest, "cannot modify transaction with this transaction time")
ErrCannotDeleteTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 16, http.StatusBadRequest, "cannot delete transaction with this transaction time")
ErrCannotAddTransactionToParentAccount = NewNormalError(NormalSubcategoryTransaction, 14, http.StatusBadRequest, "cannot add transaction to parent account")
ErrCannotModifyTransactionInParentAccount = NewNormalError(NormalSubcategoryTransaction, 15, http.StatusBadRequest, "cannot modify transaction of parent account")
ErrCannotDeleteTransactionInParentAccount = NewNormalError(NormalSubcategoryTransaction, 16, http.StatusBadRequest, "cannot delete transaction in parent account")
ErrCannotCreateTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 17, http.StatusBadRequest, "cannot add transaction with this transaction time")
ErrCannotModifyTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 18, http.StatusBadRequest, "cannot modify transaction with this transaction time")
ErrCannotDeleteTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 19, http.StatusBadRequest, "cannot delete transaction with this transaction time")
ErrCannotUseHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 20, http.StatusBadRequest, "cannot use hidden account")
ErrCannotUseHiddenTransactionCategory = NewNormalError(NormalSubcategoryTransaction, 21, http.StatusBadRequest, "cannot use hidden transaction category")
ErrCannotUseHiddenTransactionTag = NewNormalError(NormalSubcategoryTransaction, 22, http.StatusBadRequest, "cannot use hidden transaction tag")
)
+11 -7
View File
@@ -4,11 +4,15 @@ import "net/http"
// Error codes related to transaction categories
var (
ErrTransactionCategoryIdInvalid = NewNormalError(NormalSubcategoryCategory, 0, http.StatusBadRequest, "transaction category id is invalid")
ErrTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 1, http.StatusBadRequest, "transaction category not found")
ErrTransactionCategoryTypeInvalid = NewNormalError(NormalSubcategoryCategory, 2, http.StatusBadRequest, "transaction category type is invalid")
ErrParentTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 3, http.StatusBadRequest, "parent transaction category not found")
ErrCannotAddToSecondaryTransactionCategory = NewNormalError(NormalSubcategoryCategory, 4, http.StatusBadRequest, "cannot add to secondary transaction category")
ErrCannotUsePrimaryCategoryForTransaction = NewNormalError(NormalSubcategoryCategory, 5, http.StatusBadRequest, "cannot use primary category for transaction category")
ErrTransactionCategoryInUseCannotBeDeleted = NewNormalError(NormalSubcategoryCategory, 6, http.StatusBadRequest, "transaction category is in use and cannot be deleted")
ErrTransactionCategoryIdInvalid = NewNormalError(NormalSubcategoryCategory, 0, http.StatusBadRequest, "transaction category id is invalid")
ErrTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 1, http.StatusBadRequest, "transaction category not found")
ErrTransactionCategoryTypeInvalid = NewNormalError(NormalSubcategoryCategory, 2, http.StatusBadRequest, "transaction category type is invalid")
ErrParentTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 3, http.StatusBadRequest, "parent transaction category not found")
ErrCannotAddToSecondaryTransactionCategory = NewNormalError(NormalSubcategoryCategory, 4, http.StatusBadRequest, "cannot add to secondary transaction category")
ErrCannotUsePrimaryCategoryForTransaction = NewNormalError(NormalSubcategoryCategory, 5, http.StatusBadRequest, "cannot use primary category for transaction category")
ErrTransactionCategoryInUseCannotBeDeleted = NewNormalError(NormalSubcategoryCategory, 6, http.StatusBadRequest, "transaction category is in use and cannot be deleted")
ErrNotAllowChangePrimaryTransactionCategoryToSecondary = NewNormalError(NormalSubcategoryCategory, 7, http.StatusBadRequest, "not allow to change primary category to secondary category")
ErrNotAllowChangeSecondaryTransactionCategoryToPrimary = NewNormalError(NormalSubcategoryCategory, 8, http.StatusBadRequest, "not allow to change secondary category to primary category")
ErrNotAllowChangePrimaryTransactionType = NewNormalError(NormalSubcategoryCategory, 9, http.StatusBadRequest, "not allow to change primary category with different type")
ErrNotAllowUseSecondaryTransactionAsPrimaryCategory = NewNormalError(NormalSubcategoryCategory, 10, http.StatusBadRequest, "not allow to use secondary category as primary category")
)
+1
View File
@@ -9,4 +9,5 @@ var (
ErrTransactionTagNameIsEmpty = NewNormalError(NormalSubcategoryTag, 2, http.StatusBadRequest, "transaction tag name is empty")
ErrTransactionTagNameAlreadyExists = NewNormalError(NormalSubcategoryTag, 3, http.StatusBadRequest, "transaction tag name already exists")
ErrTransactionTagInUseCannotBeDeleted = NewNormalError(NormalSubcategoryTag, 4, http.StatusBadRequest, "transaction tag is in use and cannot be deleted")
ErrTransactionTagIndexNotFound = NewNormalError(NormalSubcategoryTag, 5, http.StatusBadRequest, "transaction tag index not found")
)
+10
View File
@@ -0,0 +1,10 @@
package errs
import "net/http"
// Error codes related to transaction templates
var (
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid")
)
+5 -5
View File
@@ -2,11 +2,11 @@ package errs
import "net/http"
// Error codes related to two factor authorization
// Error codes related to two-factor authorization
var (
ErrPasscodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 0, http.StatusUnauthorized, "passcode is invalid")
ErrTwoFactorRecoveryCodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 1, http.StatusUnauthorized, "two factor backup code is invalid")
ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NormalSubcategoryTwofactor, 2, http.StatusUnauthorized, "two factor backup code does not exist")
ErrTwoFactorIsNotEnabled = NewNormalError(NormalSubcategoryTwofactor, 3, http.StatusBadRequest, "two factor is not enabled")
ErrTwoFactorAlreadyEnabled = NewNormalError(NormalSubcategoryTwofactor, 4, http.StatusBadRequest, "two factor has already been enabled")
ErrTwoFactorRecoveryCodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 1, http.StatusUnauthorized, "two-factor backup code is invalid")
ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NormalSubcategoryTwofactor, 2, http.StatusUnauthorized, "two-factor backup code does not exist")
ErrTwoFactorIsNotEnabled = NewNormalError(NormalSubcategoryTwofactor, 3, http.StatusBadRequest, "two-factor is not enabled")
ErrTwoFactorAlreadyEnabled = NewNormalError(NormalSubcategoryTwofactor, 4, http.StatusBadRequest, "two-factor has already been enabled")
)
+28 -23
View File
@@ -6,27 +6,32 @@ import (
// Error codes related to users
var (
ErrLoginNameInvalid = NewNormalError(NormalSubcategoryUser, 0, http.StatusUnauthorized, "login name is invalid")
ErrLoginNameOrPasswordInvalid = NewNormalError(NormalSubcategoryUser, 1, http.StatusUnauthorized, "login name or password is invalid")
ErrLoginNameOrPasswordWrong = NewNormalError(NormalSubcategoryUser, 2, http.StatusUnauthorized, "login name or password is wrong")
ErrUserIdInvalid = NewNormalError(NormalSubcategoryUser, 3, http.StatusBadRequest, "user id is invalid")
ErrUsernameIsEmpty = NewNormalError(NormalSubcategoryUser, 4, http.StatusBadRequest, "username is empty")
ErrEmailIsEmpty = NewNormalError(NormalSubcategoryUser, 5, http.StatusBadRequest, "email is empty")
ErrNicknameIsEmpty = NewNormalError(NormalSubcategoryUser, 6, http.StatusBadRequest, "nickname is empty")
ErrPasswordIsEmpty = NewNormalError(NormalSubcategoryUser, 7, http.StatusBadRequest, "password is empty")
ErrUserDefaultCurrencyIsEmpty = NewNormalError(NormalSubcategoryUser, 8, http.StatusBadRequest, "user default currency is empty")
ErrUserDefaultCurrencyIsInvalid = NewNormalError(NormalSubcategoryUser, 9, http.StatusBadRequest, "user default currency is invalid")
ErrUserNotFound = NewNormalError(NormalSubcategoryUser, 10, http.StatusBadRequest, "user not found")
ErrUserPasswordWrong = NewNormalError(NormalSubcategoryUser, 11, http.StatusBadRequest, "password is wrong")
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
ErrEmailIsVerified = NewNormalError(NormalSubcategoryUser, 21, http.StatusBadRequest, "email is verified")
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
ErrLoginNameInvalid = NewNormalError(NormalSubcategoryUser, 0, http.StatusUnauthorized, "login name is invalid")
ErrLoginNameOrPasswordInvalid = NewNormalError(NormalSubcategoryUser, 1, http.StatusUnauthorized, "login name or password is invalid")
ErrLoginNameOrPasswordWrong = NewNormalError(NormalSubcategoryUser, 2, http.StatusUnauthorized, "login name or password is wrong")
ErrUserIdInvalid = NewNormalError(NormalSubcategoryUser, 3, http.StatusBadRequest, "user id is invalid")
ErrUsernameIsEmpty = NewNormalError(NormalSubcategoryUser, 4, http.StatusBadRequest, "username is empty")
ErrEmailIsEmpty = NewNormalError(NormalSubcategoryUser, 5, http.StatusBadRequest, "email is empty")
ErrNicknameIsEmpty = NewNormalError(NormalSubcategoryUser, 6, http.StatusBadRequest, "nickname is empty")
ErrPasswordIsEmpty = NewNormalError(NormalSubcategoryUser, 7, http.StatusBadRequest, "password is empty")
ErrUserDefaultCurrencyIsEmpty = NewNormalError(NormalSubcategoryUser, 8, http.StatusBadRequest, "user default currency is empty")
ErrUserDefaultCurrencyIsInvalid = NewNormalError(NormalSubcategoryUser, 9, http.StatusBadRequest, "user default currency is invalid")
ErrUserNotFound = NewNormalError(NormalSubcategoryUser, 10, http.StatusBadRequest, "user not found")
ErrUserPasswordWrong = NewNormalError(NormalSubcategoryUser, 11, http.StatusBadRequest, "password is wrong")
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
ErrEmailIsVerified = NewNormalError(NormalSubcategoryUser, 21, http.StatusBadRequest, "email is verified")
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual = NewNormalError(NormalSubcategoryUser, 23, http.StatusBadRequest, "decimal separator and digit grouping symbol cannot be equal")
ErrUserDefaultAccountIsHidden = NewNormalError(NormalSubcategoryUser, 24, http.StatusBadRequest, "user default account is hidden")
ErrNoUserAvatar = NewNormalError(NormalSubcategoryUser, 25, http.StatusBadRequest, "no user avatar")
ErrUserAvatarIsEmpty = NewNormalError(NormalSubcategoryUser, 26, http.StatusBadRequest, "user avatar is empty")
ErrUserAvatarNoExists = NewNormalError(NormalSubcategoryUser, 27, http.StatusNotFound, "user avatar not exists")
)
@@ -14,9 +14,9 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const nationalBankOfPolandDailyExchangeRateUrl = "https://www.nbp.pl/kursy/xml/en/lastaen.xml"
const nationalBankOfPolandInconvertibleCurrencyExchangeRateUrl = "https://www.nbp.pl/kursy/xml/en/lastben.xml"
const nationalBankOfPolandExchangeRateReferenceUrl = "https://www.nbp.pl/homen.aspx?f=/kursy/kursyen.htm"
const nationalBankOfPolandDailyExchangeRateUrl = "https://api.nbp.pl/api/exchangerates/tables/A?format=xml"
const nationalBankOfPolandInconvertibleCurrencyExchangeRateUrl = "https://api.nbp.pl/api/exchangerates/tables/B?format=xml"
const nationalBankOfPolandExchangeRateReferenceUrl = "https://nbp.pl/en/statistic-and-financial-reporting/rates/"
const nationalBankOfPolandDataSource = "Narodowy Bank Polski"
const nationalBankOfPolandBaseCurrency = "PLN"
@@ -30,16 +30,15 @@ type NationalBankOfPolandDataSource struct {
// NationalBankOfPolandExchangeRateData represents the whole data from National Bank of Poland
type NationalBankOfPolandExchangeRateData struct {
XMLName xml.Name `xml:"exchange_rates"`
Date string `xml:"date,attr"`
AllExchangeRates []*NationalBankOfPolandExchangeRate `xml:"mid-rate"`
XMLName xml.Name `xml:"ArrayOfExchangeRatesTable"`
Date string `xml:"ExchangeRatesTable>EffectiveDate"`
AllExchangeRates []*NationalBankOfPolandExchangeRate `xml:"ExchangeRatesTable>Rates>Rate"`
}
// NationalBankOfPolandExchangeRate represents the exchange rate data from National Bank of Poland
type NationalBankOfPolandExchangeRate struct {
Currency string `xml:"code,attr"`
Units string `xml:"units,attr"`
Rate string `xml:",chardata"`
Currency string `xml:"Code"`
Rate string `xml:"Mid"`
}
// ToLatestExchangeRateResponse returns a view-object according to original data from National Bank of Poland
@@ -95,13 +94,6 @@ func (e *NationalBankOfPolandExchangeRateData) ToLatestExchangeRateResponse(c *c
// ToLatestExchangeRate returns a data pair according to original data from National Bank of Poland
func (e *NationalBankOfPolandExchangeRate) ToLatestExchangeRate(c *core.Context) *models.LatestExchangeRate {
amount, err := utils.StringToInt64(e.Units)
if err != nil {
log.WarnfWithRequestId(c, "[national_bank_of_poland_datasource.ToLatestExchangeRate] failed to parse amount, currency is %s, amount is %s", e.Currency, e.Units)
return nil
}
rate, err := utils.StringToFloat64(e.Rate)
if err != nil {
@@ -114,7 +106,7 @@ func (e *NationalBankOfPolandExchangeRate) ToLatestExchangeRate(c *core.Context)
return nil
}
finalRate := float64(amount) / rate
finalRate := 1 / rate
if math.IsInf(finalRate, 0) {
return nil
@@ -10,11 +10,22 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const nationalBankOfPolandMinimumRequiredContent = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" +
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n" +
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\">3.8986</mid-rate>\n" +
" <mid-rate currency=\"Yuan Renminbi\" units=\"1\" code=\"CNY\">0.5941</mid-rate>\n" +
"</exchange_rates>"
const nationalBankOfPolandMinimumRequiredContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
" <ExchangeRatesTable>\n" +
" <EffectiveDate>2024-02-28</EffectiveDate>\n" +
" <Rates>\n" +
" <Rate>\n" +
" <Code>USD</Code>\n" +
" <Mid>3.9922</Mid>\n" +
" </Rate>\n" +
" <Rate>\n" +
" <Code>CNY</Code>\n" +
" <Mid>0.5545</Mid>\n" +
" </Rate>\n" +
" </Rates>\n" +
" </ExchangeRatesTable>\n" +
"</ArrayOfExchangeRatesTable>"
func TestNationalBankOfPolandDataSource_StandardDataExtractBaseCurrency(t *testing.T) {
dataSource := &NationalBankOfPolandDataSource{}
@@ -37,11 +48,11 @@ func TestNationalBankOfPolandDataSource_StandardDataExtractExchangeRates(t *test
assert.Equal(t, nil, err)
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "USD",
Rate: "0.25650233417124096",
Rate: "0.2504884524823406",
})
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "CNY",
Rate: "1.68321831341525",
Rate: "1.8034265103697025",
})
}
@@ -61,7 +72,33 @@ func TestNationalBankOfPolandDataSource_OnlyXMLHeader(t *testing.T) {
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>"))
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>"))
assert.NotEqual(t, nil, err)
}
func TestNationalBankOfPolandDataSource_EmptyArrayOfExchangeRatesTable(t *testing.T) {
dataSource := &NationalBankOfPolandDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
"</ArrayOfExchangeRatesTable>"))
assert.NotEqual(t, nil, err)
}
func TestNationalBankOfPolandDataSource_EmptyExchangeRatesTable(t *testing.T) {
dataSource := &NationalBankOfPolandDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
assert.NotEqual(t, nil, err)
}
@@ -71,9 +108,14 @@ func TestNationalBankOfPolandDataSource_EmptyExchangeRatesContent(t *testing.T)
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
"</exchange_rates>"))
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
" <Rates>\n"+
" </Rates>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
assert.NotEqual(t, nil, err)
}
@@ -83,10 +125,18 @@ func TestNationalBankOfPolandDataSource_InvalidCurrency(t *testing.T) {
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
" <mid-rate currency=\"XXX\" units=\"1\" code=\"XXX\">1</mid-rate>\n"+
"</exchange_rates>"))
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
" <Rates>\n"+
" <Rate>\n"+
" <Code>XXX</Code>\n"+
" <Mid>1</Mid>\n"+
" </Rate>\n"+
" </Rates>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
@@ -97,10 +147,18 @@ func TestNationalBankOfPolandDataSource_EmptyRate(t *testing.T) {
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\"></mid-rate>\n"+
"</exchange_rates>"))
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
" <Rates>\n"+
" <Rate>\n"+
" <Code>USD</Code>\n"+
" <Mid></Mid>\n"+
" </Rate>\n"+
" </Rates>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
@@ -111,10 +169,18 @@ func TestNationalBankOfPolandDataSource_InvalidRate(t *testing.T) {
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\">null</mid-rate>\n"+
"</exchange_rates>"))
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
" <Rates>\n"+
" <Rate>\n"+
" <Code>USD</Code>\n"+
" <Mid>null</Mid>\n"+
" </Rate>\n"+
" </Rates>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
+27
View File
@@ -1,9 +1,14 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// DefaultLanguage represents the default language
var DefaultLanguage = en
// AllLanguages represents all the supported language
// To add new languages, please refer to https://ezbookkeeping.mayswind.net/translating
var AllLanguages = map[string]*LocaleInfo{
"en": {
Content: en,
@@ -22,3 +27,25 @@ func GetLocaleTextItems(locale string) *LocaleTextItems {
return DefaultLanguage
}
func IsDecimalSeparatorEqualsDigitGroupingSymbol(decimalSeparator core.DecimalSeparator, digitGroupingSymbol core.DigitGroupingSymbol, locale string) bool {
if decimalSeparator == core.DECIMAL_SEPARATOR_DEFAULT && digitGroupingSymbol == core.DIGIT_GROUPING_SYMBOL_DEFAULT {
return false
}
if byte(decimalSeparator) == byte(digitGroupingSymbol) {
return true
}
localeTextItems := GetLocaleTextItems(locale)
if decimalSeparator == core.DECIMAL_SEPARATOR_DEFAULT {
decimalSeparator = localeTextItems.DefaultTypes.DecimalSeparator
}
if digitGroupingSymbol == core.DIGIT_GROUPING_SYMBOL_DEFAULT {
digitGroupingSymbol = localeTextItems.DefaultTypes.DigitGroupingSymbol
}
return byte(decimalSeparator) == byte(digitGroupingSymbol)
}
+10
View File
@@ -1,11 +1,21 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// LocaleTextItems represents all text items need to be translated
type LocaleTextItems struct {
DefaultTypes *DefaultTypes
VerifyEmailTextItems *VerifyEmailTextItems
ForgetPasswordMailTextItems *ForgetPasswordMailTextItems
}
type DefaultTypes struct {
DecimalSeparator core.DecimalSeparator
DigitGroupingSymbol core.DigitGroupingSymbol
}
// VerifyEmailTextItems represents text items need to be translated in verify mail
type VerifyEmailTextItems struct {
Title string
+8
View File
@@ -1,6 +1,14 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
var en = &LocaleTextItems{
DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
},
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "Verify Email",
SalutationFormat: "Hi %s,",
+8
View File
@@ -1,6 +1,14 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
var zhHans = &LocaleTextItems{
DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
},
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "验证邮箱",
SalutationFormat: "%s 您好,",
+1 -1
View File
@@ -42,7 +42,7 @@ func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
}
if requestId, exists := entry.Data[logFieldRequestId]; exists {
b.WriteString(fmt.Sprintf("[r=%s] ", requestId))
b.WriteString(fmt.Sprintf("[%s] ", requestId))
}
b.WriteString(entry.Message)
+43 -9
View File
@@ -41,37 +41,71 @@ func init() {
// SetLoggerConfiguration sets the logger according to the config
func SetLoggerConfiguration(config *settings.Config, isDisableBootLog bool) error {
var bootWriters []io.Writer
var writers []io.Writer
var defaultWriters []io.Writer
var requestWriters []io.Writer
var queryWriters []io.Writer
if !isDisableBootLog {
bootWriters = append(bootWriters, os.Stdout)
}
if config.EnableConsoleLog {
writers = append(writers, os.Stdout)
defaultWriters = append(defaultWriters, os.Stdout)
requestWriters = append(requestWriters, os.Stdout)
queryWriters = append(queryWriters, os.Stdout)
}
if config.EnableFileLog {
logFile, err := os.OpenFile(config.FileLogPath, os.O_CREATE|os.O_WRONLY, 0666)
defaultWriter, err := NewRotateFileWriter(config.FileLogPath, config.LogFileRotate, int64(config.LogFileMaxSize), config.LogFileMaxDays)
if err != nil {
return err
}
if !isDisableBootLog {
bootWriters = append(bootWriters, logFile)
bootWriters = append(bootWriters, defaultWriter)
}
writers = append(writers, logFile)
defaultWriters = append(defaultWriters, defaultWriter)
if config.EnableRequestLog {
if config.RequestFileLogPath != "" && config.RequestFileLogPath != config.FileLogPath {
requestWriter, err := NewRotateFileWriter(config.RequestFileLogPath, config.LogFileRotate, int64(config.LogFileMaxSize), config.LogFileMaxDays)
if err != nil {
return err
}
requestWriters = append(requestWriters, requestWriter)
} else {
requestWriters = append(requestWriters, defaultWriter)
}
}
if config.EnableQueryLog {
if config.QueryFileLogPath != "" && config.QueryFileLogPath != config.FileLogPath {
queryWriter, err := NewRotateFileWriter(config.QueryFileLogPath, config.LogFileRotate, int64(config.LogFileMaxSize), config.LogFileMaxDays)
if err != nil {
return err
}
queryWriters = append(queryWriters, queryWriter)
} else {
queryWriters = append(queryWriters, defaultWriter)
}
}
}
bootMultipleWriter := io.MultiWriter(bootWriters...)
multipleWriter := io.MultiWriter(writers...)
defaultMultipleWriter := io.MultiWriter(defaultWriters...)
requestMultipleWriter := io.MultiWriter(requestWriters...)
queryMultipleWriter := io.MultiWriter(queryWriters...)
bootLogger.SetOutput(bootMultipleWriter)
defaultLogger.SetOutput(multipleWriter)
requestLogger.SetOutput(multipleWriter)
sqlQueryLogger.SetOutput(multipleWriter)
defaultLogger.SetOutput(defaultMultipleWriter)
requestLogger.SetOutput(requestMultipleWriter)
sqlQueryLogger.SetOutput(queryMultipleWriter)
if config.LogLevel == settings.LOGLEVEL_DEBUG {
bootLogger.SetLevel(logrus.DebugLevel)
+195
View File
@@ -0,0 +1,195 @@
package log
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
const (
logRotateSuffixDateFormat = "20060102150405"
)
type RotateFileWriter struct {
EnableRotate bool
MaxFileSize int64
MaxFileDays uint32
filePath string
file *os.File
totalSize int64
mutex sync.Mutex
lastRemoveOldFilesDay int
}
var logFallbackLogger = logrus.New()
func init() {
logFallbackLogger.SetFormatter(&LogFormatter{})
logFallbackLogger.SetOutput(os.Stdout)
logFallbackLogger.SetLevel(logrus.InfoLevel)
}
// NewRotateFileWriter returns a new rotate file writer
func NewRotateFileWriter(filePath string, enableRotate bool, maxFileSize int64, maxFileDays uint32) (*RotateFileWriter, error) {
writer := &RotateFileWriter{
EnableRotate: enableRotate,
MaxFileSize: maxFileSize,
MaxFileDays: maxFileDays,
filePath: filePath,
totalSize: 0,
}
err := writer.openFile()
if err != nil {
return nil, err
}
return writer, nil
}
// Write does log data to specified file
func (w *RotateFileWriter) Write(p []byte) (n int, err error) {
dataSize := int64(len(p))
if w.EnableRotate && w.totalSize > 0 && w.totalSize+dataSize >= w.MaxFileSize {
w.mutex.Lock()
defer w.mutex.Unlock()
if w.EnableRotate && w.totalSize > 0 && w.totalSize+dataSize >= w.MaxFileSize {
err := w.rotateFile()
if err != nil {
logFallbackLogger.Errorf("[rotate_file_writer.Write] cannot rotate log file \"%s\", because %s", w.file.Name(), err.Error())
return 0, err
}
}
}
writeSize, err := w.file.Write(p)
if err != nil {
return 0, err
}
w.totalSize += int64(writeSize)
if w.EnableRotate {
today := time.Now().Day()
if today != w.lastRemoveOldFilesDay && w.MaxFileDays > 0 {
w.lastRemoveOldFilesDay = today
go w.removeOldFiles()
}
}
return writeSize, err
}
func (w *RotateFileWriter) rotateFile() error {
currentFileName := w.file.Name()
err := w.file.Close()
if err != nil {
return errs.NewLoggingError(fmt.Sprintf("cannot close log file \"%s\", because %s", w.file.Name(), err.Error()), err)
}
w.file = nil
archiveFileName := fmt.Sprintf("%s.%s", currentFileName, time.Now().Format(logRotateSuffixDateFormat))
err = os.Rename(currentFileName, archiveFileName)
if err != nil {
return errs.NewLoggingError(fmt.Sprintf("cannot rename log file \"%s\" to \"%s\", because %s", currentFileName, archiveFileName, err.Error()), err)
}
err = w.openFile()
if err != nil {
return err
}
return nil
}
func (w *RotateFileWriter) openFile() error {
if w.file != nil {
logFallbackLogger.Warnf("[rotate_file_writer.removeOldFiles] cannot reopen log file \"%s\"", w.file.Name())
return nil
}
file, err := os.OpenFile(w.filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return errs.NewLoggingError(fmt.Sprintf("cannot open log file \"%s\", because %s", w.filePath, err.Error()), err)
}
w.file = file
return nil
}
func (w *RotateFileWriter) removeOldFiles() {
dir := filepath.Dir(w.filePath)
logBaseFileName := filepath.Base(w.filePath) + "."
allLogFiles, err := os.ReadDir(dir)
if err != nil {
return
}
retainMinUnixTime := int64(0)
if w.MaxFileDays > 0 {
retainMinUnixTime = time.Now().AddDate(0, 0, -int(w.MaxFileDays)).Unix()
}
for _, file := range allLogFiles {
if file.IsDir() {
continue
}
logFileName := filepath.Base(file.Name())
if !strings.HasPrefix(logFileName, logBaseFileName) {
continue
}
rotateDate := logFileName[len(logBaseFileName):]
dotIndex := strings.Index(rotateDate, ".")
if dotIndex > 0 {
rotateDate = rotateDate[0:dotIndex]
}
if len(rotateDate) != len(logRotateSuffixDateFormat) {
logFallbackLogger.Errorf("[rotate_file_writer.removeOldFiles] date suffix of old log file \"%s\" is invalid", file.Name())
continue
}
rotateDateTime, err := time.ParseInLocation(logRotateSuffixDateFormat, rotateDate, time.Now().Location())
if err != nil {
logFallbackLogger.Errorf("[rotate_file_writer.removeOldFiles] cannot parse rotate date of old log file \"%s\", because %s", file.Name(), err.Error())
continue
}
if rotateDateTime.Unix() >= retainMinUnixTime {
continue
}
err = os.Remove(filepath.Join(dir, file.Name()))
if err != nil {
logFallbackLogger.Errorf("[rotate_file_writer.removeOldFiles] cannot remove old log file \"%s\", because %s", file.Name(), err.Error())
}
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ func JWTTwoFactorAuthorization(c *core.Context) {
}
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two factor authorization", claims.Uid)
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two-factor authorization", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenNotRequire2FA)
return
}
+1 -1
View File
@@ -16,7 +16,7 @@ func RequestId(config *settings.Config) core.MiddlewareHandlerFunc {
return
}
requestId := requestid.Container.Current.GenerateRequestId(c.ClientIP())
requestId := requestid.Container.Current.GenerateRequestId(c.ClientIP(), c.ClientPort())
c.SetRequestId(requestId)
if config.EnableRequestIdHeader {
+24 -1
View File
@@ -29,14 +29,37 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
config.MapProvider == settings.OpenTopoMapProvider ||
config.MapProvider == settings.OPNVKarteMapProvider ||
config.MapProvider == settings.CyclOSMMapProvider ||
config.MapProvider == settings.TomTomMapProvider) {
config.MapProvider == settings.CartoDBMapProvider ||
config.MapProvider == settings.TomTomMapProvider ||
config.MapProvider == settings.TianDiTuProvider ||
config.MapProvider == settings.CustomProvider) {
settingsArr = append(settingsArr, buildBooleanSetting("mp", config.EnableMapDataFetchProxy))
}
if config.MapProvider == settings.CustomProvider {
settingsArr = append(settingsArr, buildStringSetting("cmzl", fmt.Sprintf("%d-%d-%d", config.CustomMapTileServerMinZoomLevel, config.CustomMapTileServerMaxZoomLevel, config.CustomMapTileServerDefaultZoomLevel)))
if !config.EnableMapDataFetchProxy {
settingsArr = append(settingsArr, buildEncodedStringSetting("cmsu", config.CustomMapTileServerTileLayerUrl))
if config.CustomMapTileServerAnnotationLayerUrl != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("cmau", config.CustomMapTileServerAnnotationLayerUrl))
}
} else {
if config.CustomMapTileServerAnnotationLayerUrl != "" {
settingsArr = append(settingsArr, buildBooleanSetting("cmap", config.EnableMapDataFetchProxy))
}
}
}
if config.MapProvider == settings.TomTomMapProvider && config.TomTomMapAPIKey != "" && !config.EnableMapDataFetchProxy {
settingsArr = append(settingsArr, buildEncodedStringSetting("tmak", config.TomTomMapAPIKey))
}
if config.MapProvider == settings.TianDiTuProvider && config.TianDiTuAPIKey != "" && !config.EnableMapDataFetchProxy {
settingsArr = append(settingsArr, buildEncodedStringSetting("tdak", config.TianDiTuAPIKey))
}
if config.MapProvider == settings.GoogleMapProvider && config.GoogleMapAPIKey != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("gmak", config.GoogleMapAPIKey))
}
+10 -9
View File
@@ -69,15 +69,16 @@ type Account struct {
// AccountCreateRequest represents all parameters of account creation request
type AccountCreateRequest struct {
Name string `json:"name" binding:"required,notBlank,max=32"`
Category AccountCategory `json:"category" binding:"required"`
Type AccountType `json:"type" binding:"required"`
Icon int64 `json:"icon,string" binding:"required,min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
Balance int64 `json:"balance"`
Comment string `json:"comment" binding:"max=255"`
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Category AccountCategory `json:"category" binding:"required"`
Type AccountType `json:"type" binding:"required"`
Icon int64 `json:"icon,string" binding:"required,min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
Balance int64 `json:"balance"`
Comment string `json:"comment" binding:"max=255"`
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
ClientSessionId string `json:"clientSessionId"`
}
// AccountModifyRequest represents all parameters of account modification request
+4 -3
View File
@@ -2,9 +2,10 @@ package models
// AuthResponse returns a view-object of user authorization
type AuthResponse struct {
Token string `json:"token"`
Need2FA bool `json:"need2FA"`
User *UserBasicInfo `json:"user"`
Token string `json:"token"`
Need2FA bool `json:"need2FA"`
User *UserBasicInfo `json:"user"`
NotificationContent string `json:"notificationContent,omitempty"`
}
// RegisterResponse returns a view-object of user register response
+1
View File
@@ -0,0 +1 @@
package models
+57
View File
@@ -0,0 +1,57 @@
package models
import "fmt"
// CurrencyDisplayType represents the display type of amount with currency
type CurrencyDisplayType byte
// Currency Display Type
const (
CURRENCY_DISPLAY_TYPE_DEFAULT CurrencyDisplayType = 0
CURRENCY_DISPLAY_TYPE_NONE CurrencyDisplayType = 1
CURRENCY_DISPLAY_TYPE_SYMBOL_BEFORE_AMOUNT CurrencyDisplayType = 2
CURRENCY_DISPLAY_TYPE_SYMBOL_AFTER_AMOUNT CurrencyDisplayType = 3
CURRENCY_DISPLAY_TYPE_SYMBOL_BEFORE_AMOUNT_WITHOUT_SPACE CurrencyDisplayType = 4
CURRENCY_DISPLAY_TYPE_SYMBOL_AFTER_AMOUNT_WITHOUT_SPACE CurrencyDisplayType = 5
CURRENCY_DISPLAY_TYPE_CODE_BEFORE_AMOUNT CurrencyDisplayType = 6
CURRENCY_DISPLAY_TYPE_CODE_AFTER_AMOUNT CurrencyDisplayType = 7
CURRENCY_DISPLAY_TYPE_UNIT_BEFORE_AMOUNT CurrencyDisplayType = 8
CURRENCY_DISPLAY_TYPE_UNIT_AFTER_AMOUNT CurrencyDisplayType = 9
CURRENCY_DISPLAY_TYPE_NAME_BEFORE_AMOUNT CurrencyDisplayType = 10
CURRENCY_DISPLAY_TYPE_NAME_AFTER_AMOUNT CurrencyDisplayType = 11
CURRENCY_DISPLAY_TYPE_INVALID CurrencyDisplayType = 255
)
// String returns a textual representation of the currency display type enum
func (d CurrencyDisplayType) String() string {
switch d {
case CURRENCY_DISPLAY_TYPE_DEFAULT:
return "Default"
case CURRENCY_DISPLAY_TYPE_NONE:
return "None"
case CURRENCY_DISPLAY_TYPE_SYMBOL_BEFORE_AMOUNT:
return "Symbol Before Amount"
case CURRENCY_DISPLAY_TYPE_SYMBOL_AFTER_AMOUNT:
return "Symbol After Amount"
case CURRENCY_DISPLAY_TYPE_SYMBOL_BEFORE_AMOUNT_WITHOUT_SPACE:
return "Symbol Before Amount Without Space"
case CURRENCY_DISPLAY_TYPE_SYMBOL_AFTER_AMOUNT_WITHOUT_SPACE:
return "Symbol After Amount Without Space"
case CURRENCY_DISPLAY_TYPE_CODE_BEFORE_AMOUNT:
return "Code Before Amount"
case CURRENCY_DISPLAY_TYPE_CODE_AFTER_AMOUNT:
return "Code After Amount"
case CURRENCY_DISPLAY_TYPE_UNIT_BEFORE_AMOUNT:
return "Unit Before Amount"
case CURRENCY_DISPLAY_TYPE_UNIT_AFTER_AMOUNT:
return "Unit After Amount"
case CURRENCY_DISPLAY_TYPE_NAME_BEFORE_AMOUNT:
return "Name Before Amount"
case CURRENCY_DISPLAY_TYPE_NAME_AFTER_AMOUNT:
return "Name After Amount"
case CURRENCY_DISPLAY_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
+1
View File
@@ -11,4 +11,5 @@ type DataStatisticsResponse struct {
TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"`
TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"`
TotalTransactionCount int64 `json:"totalTransactionCount,string"`
TotalTransactionTemplateCount int64 `json:"totalTransactionTemplateCount,string"`
}
+14 -13
View File
@@ -7,13 +7,14 @@ const TokenMaxUserAgentLength = 255
// TokenRecord represents token data stored in database
type TokenRecord struct {
Uid int64 `xorm:"PK INDEX(IDX_token_record_uid_type_expired_time)"`
UserTokenId int64 `xorm:"PK"`
TokenType core.TokenType `xorm:"INDEX(IDX_token_record_uid_type_expired_time) TINYINT NOT NULL"`
Secret string `xorm:"VARCHAR(10) NOT NULL"`
UserAgent string `xorm:"VARCHAR(255)"`
CreatedUnixTime int64 `xorm:"PK"`
ExpiredUnixTime int64 `xorm:"INDEX(IDX_token_record_uid_type_expired_time)"`
Uid int64 `xorm:"PK INDEX(IDX_token_record_uid_type_expired_time)"`
UserTokenId int64 `xorm:"PK"`
TokenType core.TokenType `xorm:"INDEX(IDX_token_record_uid_type_expired_time) TINYINT NOT NULL"`
Secret string `xorm:"VARCHAR(10) NOT NULL"`
UserAgent string `xorm:"VARCHAR(255)"`
CreatedUnixTime int64 `xorm:"PK"`
ExpiredUnixTime int64 `xorm:"INDEX(IDX_token_record_uid_type_expired_time)"`
LastSeenUnixTime int64
}
// TokenRevokeRequest represents all parameters of token revoking request
@@ -23,9 +24,10 @@ type TokenRevokeRequest struct {
// TokenRefreshResponse represents all parameters of token refreshing request
type TokenRefreshResponse struct {
NewToken string `json:"newToken"`
OldTokenId string `json:"oldTokenId"`
User *UserBasicInfo `json:"user"`
NewToken string `json:"newToken,omitempty"`
OldTokenId string `json:"oldTokenId,omitempty"`
User *UserBasicInfo `json:"user"`
NotificationContent string `json:"notificationContent,omitempty"`
}
// TokenInfoResponse represents a view-object of token
@@ -33,8 +35,7 @@ type TokenInfoResponse struct {
TokenId string `json:"tokenId"`
TokenType core.TokenType `json:"tokenType"`
UserAgent string `json:"userAgent"`
CreatedAt int64 `json:"createdAt"`
ExpiredAt int64 `json:"expiredAt"`
LastSeen int64 `json:"lastSeen"`
IsCurrent bool `json:"isCurrent"`
}
@@ -53,5 +54,5 @@ func (a TokenInfoResponseSlice) Swap(i, j int) {
// Less reports whether the first item is less than the second one
func (a TokenInfoResponseSlice) Less(i, j int) bool {
return a[i].ExpiredAt > a[j].ExpiredAt
return a[i].LastSeen > a[j].LastSeen
}
+56 -51
View File
@@ -1,9 +1,7 @@
package models
import (
"fmt"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/utils"
@@ -76,6 +74,7 @@ type TransactionCreateRequest struct {
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
ClientSessionId string `json:"clientSessionId"`
}
// TransactionModifyRequest represents all parameters of transaction modification request
@@ -96,19 +95,23 @@ type TransactionModifyRequest struct {
// TransactionCountRequest represents transaction count request
type TransactionCountRequest struct {
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
}
// TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request
type TransactionListByMaxTimeRequest struct {
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
@@ -125,8 +128,10 @@ type TransactionListInMonthByPageRequest struct {
Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"`
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
@@ -135,13 +140,21 @@ type TransactionListInMonthByPageRequest struct {
// TransactionStatisticRequest represents all parameters of transaction statistic request
type TransactionStatisticRequest struct {
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
}
// TransactionStatisticTrendsRequest represents all parameters of transaction statistic trends request
type TransactionStatisticTrendsRequest struct {
YearMonthRangeRequest
UseTransactionTimezone bool `form:"use_transaction_timezone"`
}
// TransactionAmountsRequest represents all parameters of transaction amounts request
type TransactionAmountsRequest struct {
Query string `form:"query"`
Query string `form:"query"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
}
// TransactionAmountsRequestItem represents an item of transaction amounts request
@@ -151,12 +164,6 @@ type TransactionAmountsRequestItem struct {
EndTime int64
}
// TransactionMonthAmountsRequest represents all parameters of transaction month amounts request
type TransactionMonthAmountsRequest struct {
StartYearMonth string `form:"start_year_month"`
EndYearMonth string `form:"end_year_month"`
}
// TransactionGetRequest represents all parameters of transaction getting request
type TransactionGetRequest struct {
Id int64 `form:"id,string" binding:"required,min=1"`
@@ -170,14 +177,10 @@ type TransactionDeleteRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
}
// TransactionAccountsAmount represents transaction accounts amount map
type TransactionAccountsAmount map[int64]*TransactionAccountAmount
// TransactionAccountAmount represents transaction account amount
type TransactionAccountAmount struct {
AccountId int64
TotalIncomeAmount int64
TotalExpenseAmount int64
// YearMonthRangeRequest represents all parameters of a request with year and month range
type YearMonthRangeRequest struct {
StartYearMonth string `form:"start_year_month"`
EndYearMonth string `form:"end_year_month"`
}
// TransactionGeoLocationResponse represents a view-object of transaction geographic location info
@@ -227,7 +230,7 @@ type TransactionInfoPageWrapperResponse2 struct {
TotalCount int64 `json:"totalCount"`
}
// TransactionStatisticResponse represents an item of transaction amounts
// TransactionStatisticResponse represents transaction statistic response
type TransactionStatisticResponse struct {
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
@@ -241,6 +244,13 @@ type TransactionStatisticResponseItem struct {
TotalAmount int64 `json:"amount"`
}
// TransactionStatisticTrendsItem represents the data within each statistic interval
type TransactionStatisticTrendsItem struct {
Year int32 `json:"year"`
Month int32 `json:"month"`
Items []*TransactionStatisticResponseItem `json:"items"`
}
// TransactionAmountsResponseItem represents an item of transaction amounts
type TransactionAmountsResponseItem struct {
StartTime int64 `json:"startTime"`
@@ -380,33 +390,28 @@ func (t *TransactionAmountsRequest) GetTransactionAmountsRequestItems() ([]*Tran
return requestItems, nil
}
// GetStartTimeAndEndTime returns start unix time and end unix time by request parameter
func (t *TransactionMonthAmountsRequest) GetStartTimeAndEndTime(utcOffset int16) (int64, int64, error) {
startUnixTime := int64(0)
endUnixTime := time.Now().Unix()
// GetNumericYearMonthRange returns numeric start year, start month, end year and end month
func (t *YearMonthRangeRequest) GetNumericYearMonthRange() (int32, int32, int32, int32, error) {
var startYear, startMonth, endYear, endMonth int32
var err error
if t.StartYearMonth != "" {
startTime, err := utils.ParseFromShortDateTime(fmt.Sprintf("%s-1 0:0:0", t.StartYearMonth), utcOffset)
startYear, startMonth, err = utils.ParseNumericYearMonth(t.StartYearMonth)
if err != nil {
return 0, 0, err
return 0, 0, 0, 0, err
}
startUnixTime = startTime.Unix()
}
if t.EndYearMonth != "" {
endTime, err := utils.ParseFromShortDateTime(fmt.Sprintf("%s-1 0:0:0", t.EndYearMonth), utcOffset)
endYear, endMonth, err = utils.ParseNumericYearMonth(t.EndYearMonth)
if err != nil {
return 0, 0, err
return 0, 0, 0, 0, err
}
endTime = endTime.AddDate(0, 1, 0)
endUnixTime = endTime.Unix() - 1
}
return startUnixTime, endUnixTime, nil
return startYear, startMonth, endYear, endMonth, nil
}
// TransactionInfoResponseSlice represents the slice data structure of TransactionInfoResponse
@@ -431,26 +436,26 @@ func (s TransactionInfoResponseSlice) Less(i, j int) bool {
return s[i].Id > s[j].Id
}
// TransactionMonthAmountsResponseItemSlice represents the slice data structure of TransactionMonthAmountsResponseItem
type TransactionMonthAmountsResponseItemSlice []*TransactionMonthAmountsResponseItem
// TransactionStatisticTrendsItemSlice represents the slice data structure of TransactionStatisticTrendsItem
type TransactionStatisticTrendsItemSlice []*TransactionStatisticTrendsItem
// Len returns the count of items
func (s TransactionMonthAmountsResponseItemSlice) Len() int {
func (s TransactionStatisticTrendsItemSlice) Len() int {
return len(s)
}
// Swap swaps two items
func (s TransactionMonthAmountsResponseItemSlice) Swap(i, j int) {
func (s TransactionStatisticTrendsItemSlice) 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 TransactionMonthAmountsResponseItemSlice) Less(i, j int) bool {
func (s TransactionStatisticTrendsItemSlice) Less(i, j int) bool {
if s[i].Year != s[j].Year {
return s[i].Year > s[j].Year
return s[i].Year < s[j].Year
}
return s[i].Month > s[j].Month
return s[i].Month < s[j].Month
}
// TransactionAmountsResponseItemAmountInfoSlice represents the slice data structure of TransactionAmountsResponseItemAmountInfo
+14 -12
View File
@@ -44,12 +44,13 @@ type TransactionCategoryGetRequest struct {
// TransactionCategoryCreateRequest represents all parameters of single transaction category creation request
type TransactionCategoryCreateRequest struct {
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionCategoryType `json:"type" binding:"required"`
ParentId int64 `json:"parentId,string" binding:"min=0"`
Icon int64 `json:"icon,string" binding:"min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Comment string `json:"comment" binding:"max=255"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionCategoryType `json:"type" binding:"required"`
ParentId int64 `json:"parentId,string" binding:"min=0"`
Icon int64 `json:"icon,string" binding:"min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Comment string `json:"comment" binding:"max=255"`
ClientSessionId string `json:"clientSessionId"`
}
// TransactionCategoryCreateBatchRequest represents all parameters of transaction category batch creation request
@@ -69,12 +70,13 @@ type TransactionCategoryCreateWithSubCategories struct {
// TransactionCategoryModifyRequest represents all parameters of transaction category modification request
type TransactionCategoryModifyRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Icon int64 `json:"icon,string" binding:"min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Comment string `json:"comment" binding:"max=255"`
Hidden bool `json:"hidden"`
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
ParentId int64 `json:"parentId,string" binding:"min=0"`
Icon int64 `json:"icon,string" binding:"min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Comment string `json:"comment" binding:"max=255"`
Hidden bool `json:"hidden"`
}
// TransactionCategoryHideRequest represents all parameters of transaction category hiding request
+4 -4
View File
@@ -3,10 +3,10 @@ package models
// TransactionTag represents transaction tag data stored in database
type TransactionTag struct {
TagId int64 `xorm:"PK"`
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_name) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_name) NOT NULL"`
Name string `xorm:"INDEX(IDX_tag_uid_deleted_name) VARCHAR(32) NOT NULL"`
DisplayOrder int32 `xorm:"NOT NULL"`
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
Name string `xorm:"VARCHAR(32) NOT NULL"`
DisplayOrder int32 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
Hidden bool `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
+4 -4
View File
@@ -3,11 +3,11 @@ package models
// TransactionTagIndex represents transaction and transaction tag relation stored in database
type TransactionTagIndex struct {
TagIndexId int64 `xorm:"PK"`
Uid int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_time) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id)"`
Deleted bool `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_time) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id) NOT NULL"`
TagId int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_time)"`
Uid int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_time_tag_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id)"`
Deleted bool `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_time_tag_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id) NOT NULL"`
TransactionTime int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_transaction_time_tag_id) NOT NULL"`
TagId int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_time_tag_id)"`
TransactionId int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id)"`
TransactionTime int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_time) NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
+171
View File
@@ -0,0 +1,171 @@
package models
import (
"strings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TransactionTemplateType represents transaction template type in database
type TransactionTemplateType byte
// Transaction template types
const (
TRANSACTION_TEMPLATE_TYPE_NORMAL TransactionTemplateType = 1
)
// TransactionTemplate represents transaction template stored in database
type TransactionTemplate struct {
TemplateId int64 `xorm:"PK"`
Uid int64 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Name string `xorm:"VARCHAR(32) NOT NULL"`
Type TransactionType `xorm:"NOT NULL"`
CategoryId int64 `xorm:"NOT NULL"`
AccountId int64 `xorm:"NOT NULL"`
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
Amount int64 `xorm:"NOT NULL"`
RelatedAccountId int64 `xorm:"NOT NULL"`
RelatedAccountAmount int64 `xorm:"NOT NULL"`
HideAmount bool `xorm:"NOT NULL"`
Comment string `xorm:"VARCHAR(255) NOT NULL"`
DisplayOrder int32 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Hidden bool `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
}
// TransactionTemplateListRequest represents all parameters of transaction template list request
type TransactionTemplateListRequest struct {
TemplateType TransactionTemplateType `form:"templateType"`
}
// TransactionTemplateGetRequest represents all parameters of transaction template getting request
type TransactionTemplateGetRequest struct {
Id int64 `form:"id,string" binding:"required,min=1"`
}
// TransactionTemplateCreateRequest represents all parameters of transaction template creation request
type TransactionTemplateCreateRequest struct {
TemplateType TransactionTemplateType `json:"templateType"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
ClientSessionId string `json:"clientSessionId"`
}
// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request
type TransactionTemplateModifyNameRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
}
// TransactionTemplateModifyRequest represents all parameters of transaction template modification request
type TransactionTemplateModifyRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
}
// TransactionTemplateHideRequest represents all parameters of transaction template hiding request
type TransactionTemplateHideRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Hidden bool `json:"hidden"`
}
// TransactionTemplateMoveRequest represents all parameters of transaction template moving request
type TransactionTemplateMoveRequest struct {
NewDisplayOrders []*TransactionTemplateNewDisplayOrderRequest `json:"newDisplayOrders"`
}
// TransactionTemplateNewDisplayOrderRequest represents a data pair of id and display order
type TransactionTemplateNewDisplayOrderRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
DisplayOrder int32 `json:"displayOrder"`
}
// TransactionTemplateDeleteRequest represents all parameters of transaction template deleting request
type TransactionTemplateDeleteRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
}
type TransactionTemplateInfoResponse struct {
*TransactionInfoResponse
TemplateType TransactionTemplateType `json:"templateType"`
Name string `json:"name"`
DisplayOrder int32 `json:"displayOrder"`
Hidden bool `json:"hidden"`
}
// ToTransactionInfoResponse returns a view-object according to database model
func (t *TransactionTemplate) ToTransactionInfoResponse(utcOffset int16) *TransactionInfoResponse {
tagIds := make([]string, 0, 0)
if t.TagIds != "" {
tagIds = strings.Split(t.TagIds, ",")
}
return &TransactionInfoResponse{
Id: t.TemplateId,
TimeSequenceId: utils.GetMinTransactionTimeFromUnixTime(t.CreatedUnixTime),
Type: t.Type,
CategoryId: t.CategoryId,
Time: 0,
UtcOffset: utcOffset,
SourceAccountId: t.AccountId,
DestinationAccountId: t.RelatedAccountId,
SourceAmount: t.Amount,
DestinationAmount: t.RelatedAccountAmount,
HideAmount: t.HideAmount,
TagIds: tagIds,
Comment: t.Comment,
GeoLocation: nil,
Editable: true,
}
}
// ToTransactionTemplateInfoResponse returns a view-object according to database model
func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(utcOffset int16) *TransactionTemplateInfoResponse {
return &TransactionTemplateInfoResponse{
TransactionInfoResponse: t.ToTransactionInfoResponse(utcOffset),
TemplateType: t.TemplateType,
Name: t.Name,
DisplayOrder: t.DisplayOrder,
Hidden: t.Hidden,
}
}
// TransactionTemplateInfoResponseSlice represents the slice data structure of TransactionTemplateInfoResponse
type TransactionTemplateInfoResponseSlice []*TransactionTemplateInfoResponse
// Len returns the count of items
func (s TransactionTemplateInfoResponseSlice) Len() int {
return len(s)
}
// Swap swaps two items
func (s TransactionTemplateInfoResponseSlice) 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 TransactionTemplateInfoResponseSlice) Less(i, j int) bool {
return s[i].DisplayOrder < s[j].DisplayOrder
}
+107 -73
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -11,7 +12,7 @@ import (
// TransactionEditScope represents the scope which transaction can be edited
type TransactionEditScope byte
// Editable Transaction Scopes
// Editable Transaction Ranges
const (
TRANSACTION_EDIT_SCOPE_NONE TransactionEditScope = 0
TRANSACTION_EDIT_SCOPE_ALL TransactionEditScope = 1
@@ -23,7 +24,7 @@ const (
TRANSACTION_EDIT_SCOPE_INVALID TransactionEditScope = 255
)
// String returns a textual representation of the editable transaction scopes enum
// String returns a textual representation of the editable transaction ranges enum
func (s TransactionEditScope) String() string {
switch s {
case TRANSACTION_EDIT_SCOPE_NONE:
@@ -47,6 +48,39 @@ func (s TransactionEditScope) String() string {
}
}
// AmountColorType represents the type of amount color in frontend
type AmountColorType byte
// Amount Color Types
const (
AMOUNT_COLOR_TYPE_DEFAULT AmountColorType = 0
AMOUNT_COLOR_TYPE_GREEN AmountColorType = 1
AMOUNT_COLOR_TYPE_RED AmountColorType = 2
AMOUNT_COLOR_TYPE_YELLOW AmountColorType = 3
AMOUNT_COLOR_TYPE_BLACK_OR_WHITE AmountColorType = 4
AMOUNT_COLOR_TYPE_INVALID AmountColorType = 255
)
// String returns a textual representation of the amount color type enum
func (s AmountColorType) String() string {
switch s {
case AMOUNT_COLOR_TYPE_DEFAULT:
return "Default"
case AMOUNT_COLOR_TYPE_GREEN:
return "Green"
case AMOUNT_COLOR_TYPE_RED:
return "Red"
case AMOUNT_COLOR_TYPE_YELLOW:
return "Yellow"
case AMOUNT_COLOR_TYPE_BLACK_OR_WHITE:
return "Black or White"
case AMOUNT_COLOR_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(s))
}
}
// User represents user data stored in database
type User struct {
Uid int64 `xorm:"PK"`
@@ -55,15 +89,22 @@ type User struct {
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
Password string `xorm:"VARCHAR(64) NOT NULL"`
Salt string `xorm:"VARCHAR(10) NOT NULL"`
CustomAvatarType string `xorm:"VARCHAR(10)"`
DefaultAccountId int64
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
Language string `xorm:"VARCHAR(10)"`
DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"`
FirstDayOfWeek WeekDay `xorm:"TINYINT NOT NULL"`
LongDateFormat LongDateFormat `xorm:"TINYINT"`
ShortDateFormat ShortDateFormat `xorm:"TINYINT"`
LongTimeFormat LongTimeFormat `xorm:"TINYINT"`
ShortTimeFormat ShortTimeFormat `xorm:"TINYINT"`
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
Language string `xorm:"VARCHAR(10)"`
DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"`
FirstDayOfWeek WeekDay `xorm:"TINYINT NOT NULL"`
LongDateFormat LongDateFormat `xorm:"TINYINT"`
ShortDateFormat ShortDateFormat `xorm:"TINYINT"`
LongTimeFormat LongTimeFormat `xorm:"TINYINT"`
ShortTimeFormat ShortTimeFormat `xorm:"TINYINT"`
DecimalSeparator core.DecimalSeparator `xorm:"TINYINT"`
DigitGroupingSymbol core.DigitGroupingSymbol `xorm:"TINYINT"`
DigitGrouping core.DigitGroupingType `xorm:"TINYINT"`
CurrencyDisplayType CurrencyDisplayType `xorm:"TINYINT"`
ExpenseAmountColor AmountColorType `xorm:"TINYINT"`
IncomeAmountColor AmountColorType `xorm:"TINYINT"`
Disabled bool
Deleted bool `xorm:"NOT NULL"`
EmailVerified bool `xorm:"NOT NULL"`
@@ -75,21 +116,27 @@ type User struct {
// UserBasicInfo represents a view-object of user basic info
type UserBasicInfo struct {
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar"`
AvatarProvider string `json:"avatarProvider,omitempty"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
EmailVerified bool `json:"emailVerified"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar"`
AvatarProvider string `json:"avatarProvider,omitempty"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
DecimalSeparator core.DecimalSeparator `json:"decimalSeparator"`
DigitGroupingSymbol core.DigitGroupingSymbol `json:"digitGroupingSymbol"`
DigitGrouping core.DigitGroupingType `json:"digitGrouping"`
CurrencyDisplayType CurrencyDisplayType `json:"currencyDisplayType"`
ExpenseAmountColor AmountColorType `json:"expenseAmountColor"`
IncomeAmountColor AmountColorType `json:"incomeAmountColor"`
EmailVerified bool `json:"emailVerified"`
}
// UserLoginRequest represents all parameters of user login request
@@ -117,8 +164,9 @@ type UserVerifyEmailRequest struct {
// UserVerifyEmailResponse represents all response parameters after user have verified email
type UserVerifyEmailResponse struct {
NewToken string `json:"newToken,omitempty"`
User *UserBasicInfo `json:"user"`
NewToken string `json:"newToken,omitempty"`
User *UserBasicInfo `json:"user"`
NotificationContent string `json:"notificationContent,omitempty"`
}
// UserResendVerifyEmailRequest represents all parameters of user resend verify email request
@@ -129,19 +177,25 @@ type UserResendVerifyEmailRequest struct {
// UserProfileUpdateRequest represents all parameters of user updating profile request
type UserProfileUpdateRequest struct {
Email string `json:"email" binding:"omitempty,notBlank,max=100,validEmail"`
Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
OldPassword string `json:"oldPassword" binding:"omitempty,min=6,max=128"`
DefaultAccountId int64 `json:"defaultAccountId,string" binding:"omitempty,min=1"`
TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=7"`
Language string `json:"language" binding:"omitempty,min=2,max=16"`
DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"`
FirstDayOfWeek *WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"`
LongDateFormat *LongDateFormat `json:"longDateFormat" binding:"omitempty,min=0,max=3"`
ShortDateFormat *ShortDateFormat `json:"shortDateFormat" binding:"omitempty,min=0,max=3"`
LongTimeFormat *LongTimeFormat `json:"longTimeFormat" binding:"omitempty,min=0,max=3"`
ShortTimeFormat *ShortTimeFormat `json:"shortTimeFormat" binding:"omitempty,min=0,max=3"`
Email string `json:"email" binding:"omitempty,notBlank,max=100,validEmail"`
Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
OldPassword string `json:"oldPassword" binding:"omitempty,min=6,max=128"`
DefaultAccountId int64 `json:"defaultAccountId,string" binding:"omitempty,min=1"`
TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=6"`
Language string `json:"language" binding:"omitempty,min=2,max=16"`
DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"`
FirstDayOfWeek *WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"`
LongDateFormat *LongDateFormat `json:"longDateFormat" binding:"omitempty,min=0,max=3"`
ShortDateFormat *ShortDateFormat `json:"shortDateFormat" binding:"omitempty,min=0,max=3"`
LongTimeFormat *LongTimeFormat `json:"longTimeFormat" binding:"omitempty,min=0,max=3"`
ShortTimeFormat *ShortTimeFormat `json:"shortTimeFormat" binding:"omitempty,min=0,max=3"`
DecimalSeparator *core.DecimalSeparator `json:"decimalSeparator" binding:"omitempty,min=0,max=3"`
DigitGroupingSymbol *core.DigitGroupingSymbol `json:"digitGroupingSymbol" binding:"omitempty,min=0,max=4"`
DigitGrouping *core.DigitGroupingType `json:"digitGrouping" binding:"omitempty,min=0,max=2"`
CurrencyDisplayType *CurrencyDisplayType `json:"currencyDisplayType" binding:"omitempty,min=0,max=11"`
ExpenseAmountColor *AmountColorType `json:"expenseAmountColor" binding:"omitempty,min=0,max=4"`
IncomeAmountColor *AmountColorType `json:"incomeAmountColor" binding:"omitempty,min=0,max=4"`
}
// UserProfileUpdateResponse represents the data returns to frontend after updating profile
@@ -152,22 +206,8 @@ type UserProfileUpdateResponse struct {
// UserProfileResponse represents a view-object of user profile
type UserProfileResponse struct {
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar"`
AvatarProvider string `json:"avatarProvider,omitempty"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
EmailVerified bool `json:"emailVerified"`
LastLoginAt int64 `json:"lastLoginAt"`
*UserBasicInfo
LastLoginAt int64 `json:"lastLoginAt"`
}
// CanEditTransactionByTransactionTime returns whether this user can edit transaction with specified transaction time
@@ -229,6 +269,12 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo {
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
DecimalSeparator: u.DecimalSeparator,
DigitGroupingSymbol: u.DigitGroupingSymbol,
DigitGrouping: u.DigitGrouping,
CurrencyDisplayType: u.CurrencyDisplayType,
ExpenseAmountColor: u.ExpenseAmountColor,
IncomeAmountColor: u.IncomeAmountColor,
EmailVerified: u.EmailVerified,
}
}
@@ -236,22 +282,8 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo {
// ToUserProfileResponse returns a user profile view-object according to database model
func (u *User) ToUserProfileResponse() *UserProfileResponse {
return &UserProfileResponse{
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
AvatarUrl: u.getAvatarUrl(),
AvatarProvider: u.getAvatarProvider(),
DefaultAccountId: u.DefaultAccountId,
TransactionEditScope: u.TransactionEditScope,
Language: u.Language,
DefaultCurrency: u.DefaultCurrency,
FirstDayOfWeek: u.FirstDayOfWeek,
LongDateFormat: u.LongDateFormat,
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
EmailVerified: u.EmailVerified,
LastLoginAt: u.LastLoginUnixTime,
UserBasicInfo: u.ToUserBasicInfo(),
LastLoginAt: u.LastLoginUnixTime,
}
}
@@ -262,7 +294,9 @@ func (u *User) getAvatarProvider() string {
func (u *User) getAvatarUrl() string {
avatarProvider := settings.Container.Current.AvatarProvider
if avatarProvider == settings.GravatarProvider {
if avatarProvider == settings.InternalAvatarProvider {
return utils.GetInternalAvatarUrl(u.Uid, u.CustomAvatarType, settings.Container.Current.RootUrl)
} else if avatarProvider == settings.GravatarProvider {
return utils.GetGravatarUrl(u.Email)
}
+35 -31
View File
@@ -6,7 +6,6 @@ import (
"encoding/hex"
"fmt"
"hash/crc32"
"math"
"net"
"sync/atomic"
"time"
@@ -19,15 +18,18 @@ import (
// Length and mask of all information in request id
const (
requestIdLength = 36
secondsTodayBits = 17
secondsTodayBitsMask = (1 << secondsTodayBits) - 1
randomNumberBits = 15
randomNumberBitsMask = (1 << randomNumberBits) - 1
reqSeqNumberBits = 31
reqSeqNumberBitsMask = (1 << reqSeqNumberBits) - 1
clientIpv6Bit = 1
clientIpv6BitMask = 1
requestIdLength = 36
secondsTodayBits = 17
secondsTodayBitsMask = (1 << secondsTodayBits) - 1
clientPortNumberAllBits = 16
clientPortNumberHigh1Bit = 1
clientPortNumberLow15Bits = clientPortNumberAllBits - clientPortNumberHigh1Bit
clientPortNumberHigh1BitMask = 1 << clientPortNumberLow15Bits
clientPortNumberLow15BitsMask = clientPortNumberHigh1BitMask - 1
reqSeqNumberBits = 30
reqSeqNumberBitsMask = (1 << reqSeqNumberBits) - 1
clientIpv6Bit = 1
clientIpv6BitMask = 1
)
// RequestIdInfo represents a struct which has all information in request id
@@ -35,10 +37,10 @@ type RequestIdInfo struct {
ServerUniqId uint16
InstanceUniqId uint16
SecondsElapsedToday uint32
RandomNumber uint32
RequestSeqId uint32
IsClientIpv6 bool
ClientIp uint32
ClientPort uint16
}
// DefaultRequestIdGenerator represents default request id generator
@@ -121,7 +123,7 @@ func (r *DefaultRequestIdGenerator) GetCurrentInstanceUniqId() uint16 {
}
// GenerateRequestId returns a new request id
func (r *DefaultRequestIdGenerator) GenerateRequestId(clientIpAddr string) string {
func (r *DefaultRequestIdGenerator) GenerateRequestId(clientIpAddr string, clientPort uint16) string {
ip := net.ParseIP(clientIpAddr)
isClientIpv6 := ip.To4() == nil
var clientIp uint32
@@ -132,38 +134,38 @@ func (r *DefaultRequestIdGenerator) GenerateRequestId(clientIpAddr string) strin
clientIp = binary.BigEndian.Uint32(ip.To4())
}
requestId := r.getRequestId(r.serverUniqId, r.instanceUniqId, isClientIpv6, clientIp)
requestId := r.getRequestId(r.serverUniqId, r.instanceUniqId, isClientIpv6, clientIp, clientPort)
return requestId
}
func (r *DefaultRequestIdGenerator) getRequestId(serverUniqId uint16, instanceUniqId uint16, clientIpV6 bool, clientIp uint32) string {
func (r *DefaultRequestIdGenerator) getRequestId(serverUniqId uint16, instanceUniqId uint16, clientIpV6 bool, clientIp uint32, clientPort uint16) string {
clientIpv6Flag := uint32(0)
if clientIpV6 {
clientIpv6Flag = uint32(1)
}
// 128bits = serverUniqId(16bits) + instanceUniqId(16bits) + secondsElapsedToday(17bits) + randomNumber(15bits) + sequentialNumber(31bits) + clientIpv6Flag(1bit) + clientIp(32bits)
// 128bits = serverUniqId(16bits) + instanceUniqId(16bits) + secondsElapsedToday(17bits) + clientPortLow15Bits(15bits) + sequentialNumber(30bits) + clientPortHigh1Bit(1bit) + clientIpv6Flag(1bit) + clientIp(32bits)
secondsElapsedToday := r.getSecondsElapsedToday()
secondsLow17bits := uint32(secondsElapsedToday & secondsTodayBitsMask)
randomNumber, _ := utils.GetRandomInteger(math.MaxInt16)
randomNumberLow15bits := uint32(randomNumber & randomNumberBitsMask)
clientPortHigh1bit := uint32((clientPort & clientPortNumberHigh1BitMask) >> clientPortNumberLow15Bits)
clientPortLow15bits := uint32(clientPort & clientPortNumberLow15BitsMask)
secondsAndRandomNumber := (secondsLow17bits << randomNumberBits) | randomNumberLow15bits
secondsAndClientPortLowBits := (secondsLow17bits << clientPortNumberLow15Bits) | clientPortLow15bits
seqId := r.requestSeqId.Add(1)
seqIdLow31bits := seqId & reqSeqNumberBitsMask
seqIdLow30bits := seqId & reqSeqNumberBitsMask
seqIdAndClientIpv6Flag := (seqIdLow31bits << clientIpv6Bit) | (clientIpv6Flag & clientIpv6BitMask)
seqIdAndClientPortHighBitAndClientIpv6Flag := (seqIdLow30bits << (clientPortNumberHigh1Bit + clientIpv6Bit)) | (clientPortHigh1bit << clientPortNumberHigh1Bit) | (clientIpv6Flag & clientIpv6BitMask)
buf := &bytes.Buffer{}
_ = binary.Write(buf, binary.BigEndian, serverUniqId)
_ = binary.Write(buf, binary.BigEndian, instanceUniqId)
_ = binary.Write(buf, binary.BigEndian, secondsAndRandomNumber)
_ = binary.Write(buf, binary.BigEndian, seqIdAndClientIpv6Flag)
_ = binary.Write(buf, binary.BigEndian, secondsAndClientPortLowBits)
_ = binary.Write(buf, binary.BigEndian, seqIdAndClientPortHighBitAndClientIpv6Flag)
_ = binary.Write(buf, binary.BigEndian, clientIp)
return r.getUuidFromRequestId(buf)
@@ -198,23 +200,25 @@ func (r *DefaultRequestIdGenerator) parseRequestIdInfo(data []byte) *RequestIdIn
var serverUniqId uint16
var instanceUniqId uint16
var secondsAndRandomNumber uint32
var seqIdAndClientIpv6Flag uint32
var secondsAndClientPortLowBits uint32
var seqIdAndClientPortHighBitAndClientIpv6Flag uint32
var clientIp uint32
_ = binary.Read(buf, binary.BigEndian, &serverUniqId)
_ = binary.Read(buf, binary.BigEndian, &instanceUniqId)
_ = binary.Read(buf, binary.BigEndian, &secondsAndRandomNumber)
_ = binary.Read(buf, binary.BigEndian, &seqIdAndClientIpv6Flag)
_ = binary.Read(buf, binary.BigEndian, &secondsAndClientPortLowBits)
_ = binary.Read(buf, binary.BigEndian, &seqIdAndClientPortHighBitAndClientIpv6Flag)
_ = binary.Read(buf, binary.BigEndian, &clientIp)
secondsElapsedToday := (secondsAndRandomNumber >> randomNumberBits) & secondsTodayBitsMask
randomNumber := (secondsAndRandomNumber & randomNumberBitsMask)
secondsElapsedToday := (secondsAndClientPortLowBits >> clientPortNumberLow15Bits) & secondsTodayBitsMask
seqId := (seqIdAndClientIpv6Flag >> clientIpv6Bit) & reqSeqNumberBitsMask
isClientIpv6Flag := (seqIdAndClientIpv6Flag & clientIpv6BitMask)
seqId := (seqIdAndClientPortHighBitAndClientIpv6Flag >> (clientPortNumberHigh1Bit + clientIpv6Bit)) & reqSeqNumberBitsMask
clientPortHigh1Bit := ((seqIdAndClientPortHighBitAndClientIpv6Flag >> clientIpv6Bit) << clientPortNumberLow15Bits) & clientPortNumberHigh1BitMask
isClientIpv6Flag := seqIdAndClientPortHighBitAndClientIpv6Flag & clientIpv6BitMask
isClientIpv6 := false
clientPort := uint16(clientPortHigh1Bit | (secondsAndClientPortLowBits & clientPortNumberLow15BitsMask))
if isClientIpv6Flag == 1 {
isClientIpv6 = true
}
@@ -224,9 +228,9 @@ func (r *DefaultRequestIdGenerator) parseRequestIdInfo(data []byte) *RequestIdIn
InstanceUniqId: instanceUniqId,
SecondsElapsedToday: secondsElapsedToday,
RequestSeqId: seqId,
RandomNumber: randomNumber,
IsClientIpv6: isClientIpv6,
ClientIp: clientIp,
ClientPort: clientPort,
}
}
@@ -10,7 +10,7 @@ import (
func TestNewDefaultRequestIdGenerator_Http(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "123.234.123.234", HttpPort: 8080, SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("127.0.0.1")
requestId := generator.GenerateRequestId("127.0.0.1", 20000)
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedServerUniqId := uint16(0x2476) // crc32("123.234.123.234" + "_" + "secretkey") & 0xFFFF
@@ -24,7 +24,7 @@ func TestNewDefaultRequestIdGenerator_Http(t *testing.T) {
func TestNewDefaultRequestIdGenerator_UnixSocket(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", UnixSocketPath: "/var/lib/ezbookkeeping/ezbookkeeping.sock", Protocol: "socket", SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("127.0.0.1")
requestId := generator.GenerateRequestId("127.0.0.1", 20000)
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedServerUniqId := uint16(0x5bdb) // crc32("1.2.3.4" + "_" + "secretkey") & 0xFFFF
@@ -38,7 +38,7 @@ func TestNewDefaultRequestIdGenerator_UnixSocket(t *testing.T) {
func TestNewDefaultRequestIdGenerator_ClientIpv4(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", UnixSocketPath: "/var/lib/ezbookkeeping/ezbookkeeping.sock", Protocol: "socket", SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("127.0.0.1")
requestId := generator.GenerateRequestId("127.0.0.1", 20000)
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientIp := uint32(0x7f000001) // 127.0.0.1
@@ -49,7 +49,7 @@ func TestNewDefaultRequestIdGenerator_ClientIpv4(t *testing.T) {
actualClientIpv6 := requestIdInfo.IsClientIpv6
assert.Equal(t, expectedClientIpv6, actualClientIpv6)
requestId = generator.GenerateRequestId("192.168.1.100")
requestId = generator.GenerateRequestId("192.168.1.100", 20000)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientIp = uint32(0xc0a80164) // 192.168.1.100
@@ -63,7 +63,7 @@ func TestNewDefaultRequestIdGenerator_ClientIpv4(t *testing.T) {
func TestNewDefaultRequestIdGenerator_ClientIpv6(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", UnixSocketPath: "/var/lib/ezbookkeeping/ezbookkeeping.sock", Protocol: "socket", SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("2001:abc:def:1234::1")
requestId := generator.GenerateRequestId("2001:abc:def:1234::1", 20000)
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientIp := uint32(0x76fe1b98) // crc32("2001:abc:def:1234::1")
@@ -74,7 +74,7 @@ func TestNewDefaultRequestIdGenerator_ClientIpv6(t *testing.T) {
actualClientIpv6 := requestIdInfo.IsClientIpv6
assert.Equal(t, expectedClientIpv6, actualClientIpv6)
requestId = generator.GenerateRequestId("2400:abcd:1234:1:56ef:ab78:c90d:1e2f")
requestId = generator.GenerateRequestId("2400:abcd:1234:1:56ef:ab78:c90d:1e2f", 20000)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientIp = uint32(0xa0a25faa) // crc32("2400:abcd:1234:1:56ef:ab78:c90d:1e2f")
@@ -86,11 +86,56 @@ func TestNewDefaultRequestIdGenerator_ClientIpv6(t *testing.T) {
assert.Equal(t, expectedClientIpv6, actualClientIpv6)
}
func TestNewDefaultRequestIdGenerator_ClientPort(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", UnixSocketPath: "/var/lib/ezbookkeeping/ezbookkeeping.sock", Protocol: "socket", SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("127.0.0.1", 0)
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort := uint16(0)
actualClientPort := requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 12345)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(12345)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 32767)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(32767)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 32768)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(32768)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 56789)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(56789)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 65535)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(65535)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
}
func TestGenerateRequestId_100Times(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", HttpPort: 1234, SecretKey: "secretkey"})
for i := 1; i <= 100; i++ {
requestId := generator.GenerateRequestId("127.0.0.1")
requestId := generator.GenerateRequestId("127.0.0.1", 20000)
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedRequestSeqId := uint32(i)
+2 -2
View File
@@ -27,6 +27,6 @@ func InitializeRequestIdGenerator(config *settings.Config) error {
}
// GenerateRequestId returns a new request id by the current request id generator
func (u *RequestIdContainer) GenerateRequestId(clientIpAddr string) string {
return u.Current.GenerateRequestId(clientIpAddr)
func (u *RequestIdContainer) GenerateRequestId(clientIpAddr string, clientPort uint16) string {
return u.Current.GenerateRequestId(clientIpAddr, clientPort)
}
+1 -1
View File
@@ -2,7 +2,7 @@ package requestid
// RequestIdGenerator is common request generator interface
type RequestIdGenerator interface {
GenerateRequestId(clientIpAddr string) string
GenerateRequestId(clientIpAddr string, clientPort uint16) string
GetCurrentServerUniqId() uint16
GetCurrentInstanceUniqId() uint16
}
+46 -3
View File
@@ -1,6 +1,7 @@
package services
import (
"strings"
"time"
"xorm.io/xorm"
@@ -54,7 +55,7 @@ func (s *AccountService) GetAllAccountsByUid(c *core.Context, uid int64) ([]*mod
return accounts, err
}
// GetAccountAndSubAccountsByAccountId returns account model and sub account models according to account id
// GetAccountAndSubAccountsByAccountId returns account model and sub-account models according to account id
func (s *AccountService) GetAccountAndSubAccountsByAccountId(c *core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
@@ -70,7 +71,7 @@ func (s *AccountService) GetAccountAndSubAccountsByAccountId(c *core.Context, ui
return accounts, err
}
// GetSubAccountsByAccountId returns sub account models according to account id
// GetSubAccountsByAccountId returns sub-account models according to account id
func (s *AccountService) GetSubAccountsByAccountId(c *core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
@@ -86,6 +87,48 @@ func (s *AccountService) GetSubAccountsByAccountId(c *core.Context, uid int64, a
return accounts, err
}
// GetSubAccountsByAccountIds returns sub-account models according to account ids
func (s *AccountService) GetSubAccountsByAccountIds(c *core.Context, uid int64, accountIds []int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if len(accountIds) <= 0 {
return nil, errs.ErrAccountIdInvalid
}
condition := "uid=? AND deleted=?"
conditionParams := make([]any, 0, len(accountIds)+2)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
var accountIdConditions strings.Builder
for i := 0; i < len(accountIds); i++ {
if accountIds[i] <= 0 {
return nil, errs.ErrAccountIdInvalid
}
if accountIdConditions.Len() > 0 {
accountIdConditions.WriteString(",")
}
accountIdConditions.WriteString("?")
conditionParams = append(conditionParams, accountIds[i])
}
if accountIdConditions.Len() > 1 {
condition = condition + " AND parent_account_id IN (" + accountIdConditions.String() + ")"
} else {
condition = condition + " AND parent_account_id = " + accountIdConditions.String()
}
var accounts []*models.Account
err := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).OrderBy("display_order asc").Find(&accounts)
return accounts, err
}
// GetAccountsByAccountIds returns account models according to account ids
func (s *AccountService) GetAccountsByAccountIds(c *core.Context, uid int64, accountIds []int64) (map[int64]*models.Account, error) {
if uid <= 0 {
@@ -127,7 +170,7 @@ func (s *AccountService) GetMaxDisplayOrder(c *core.Context, uid int64, category
}
}
// GetMaxSubAccountDisplayOrder returns the max display order of sub account according to account category and parent account id
// GetMaxSubAccountDisplayOrder returns the max display order of sub-account according to account category and parent account id
func (s *AccountService) GetMaxSubAccountDisplayOrder(c *core.Context, uid int64, category models.AccountCategory, parentAccountId int64) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
+33 -7
View File
@@ -58,7 +58,7 @@ func (s *TokenService) GetAllUnexpiredNormalTokensByUid(c *core.Context, uid int
now := time.Now().Unix()
var tokenRecords []*models.TokenRecord
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)
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time", "last_seen_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords)
return tokenRecords, err
}
@@ -98,6 +98,31 @@ func (s *TokenService) CreatePasswordResetToken(c *core.Context, user *models.Us
return s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
}
// UpdateTokenLastSeen updates the last seen time of specified token
func (s *TokenService) UpdateTokenLastSeen(c *core.Context, tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {
return errs.ErrUserIdInvalid
}
if tokenRecord.UserTokenId <= 0 {
return errs.ErrInvalidUserTokenId
}
tokenRecord.LastSeenUnixTime = time.Now().Unix()
return s.TokenDB(tokenRecord.Uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.Cols("last_seen_unix_time").Where("uid=? AND user_token_id=? AND created_unix_time=?", tokenRecord.Uid, tokenRecord.UserTokenId, tokenRecord.CreatedUnixTime).Update(tokenRecord)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTokenRecordNotFound
}
return nil
})
}
// DeleteToken deletes given token from database
func (s *TokenService) DeleteToken(c *core.Context, tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {
@@ -294,12 +319,13 @@ func (s *TokenService) createToken(c *core.Context, user *models.User, tokenType
now := time.Now()
tokenRecord := &models.TokenRecord{
Uid: user.Uid,
UserTokenId: s.getUserTokenId(),
TokenType: tokenType,
UserAgent: userAgent,
CreatedUnixTime: now.Unix(),
ExpiredUnixTime: now.Add(expiryDate).Unix(),
Uid: user.Uid,
UserTokenId: s.getUserTokenId(),
TokenType: tokenType,
UserAgent: userAgent,
CreatedUnixTime: now.Unix(),
ExpiredUnixTime: now.Add(expiryDate).Unix(),
LastSeenUnixTime: now.Unix(),
}
if tokenRecord.Secret, err = utils.GetRandomString(10); err != nil {
+44 -1
View File
@@ -1,6 +1,7 @@
package services
import (
"strings"
"time"
"xorm.io/xorm"
@@ -68,6 +69,48 @@ func (s *TransactionCategoryService) GetAllCategoriesByUid(c *core.Context, uid
return categories, err
}
// GetSubCategoriesByCategoryIds returns sub-category models according to category ids
func (s *TransactionCategoryService) GetSubCategoriesByCategoryIds(c *core.Context, uid int64, categoryIds []int64) ([]*models.TransactionCategory, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if len(categoryIds) <= 0 {
return nil, errs.ErrTransactionCategoryIdInvalid
}
condition := "uid=? AND deleted=?"
conditionParams := make([]any, 0, len(categoryIds)+2)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
var categoryIdConditions strings.Builder
for i := 0; i < len(categoryIds); i++ {
if categoryIds[i] <= 0 {
return nil, errs.ErrTransactionCategoryIdInvalid
}
if categoryIdConditions.Len() > 0 {
categoryIdConditions.WriteString(",")
}
categoryIdConditions.WriteString("?")
conditionParams = append(conditionParams, categoryIds[i])
}
if categoryIdConditions.Len() > 1 {
condition = condition + " AND parent_category_id IN (" + categoryIdConditions.String() + ")"
} else {
condition = condition + " AND parent_category_id = " + categoryIdConditions.String()
}
var categories []*models.TransactionCategory
err := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).OrderBy("display_order asc").Find(&categories)
return categories, err
}
// GetCategoryByCategoryId returns a transaction category model according to transaction category id
func (s *TransactionCategoryService) GetCategoryByCategoryId(c *core.Context, uid int64, categoryId int64) (*models.TransactionCategory, error) {
if uid <= 0 {
@@ -249,7 +292,7 @@ func (s *TransactionCategoryService) ModifyCategory(c *core.Context, category *m
category.UpdatedUnixTime = time.Now().Unix()
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)
updatedRows, err := sess.ID(category.CategoryId).Cols("parent_category_id", "name", "icon", "color", "comment", "hidden", "updated_unix_time").Where("uid=? AND deleted=?", category.Uid, false).Update(category)
if err != nil {
return err
+83 -10
View File
@@ -12,6 +12,8 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
const pageCountForLoadAllTransactionTagIndexes = 1000
// TransactionTagService represents transaction tag service
type TransactionTagService struct {
ServiceUsingDB
@@ -117,15 +119,60 @@ func (s *TransactionTagService) GetMaxDisplayOrder(c *core.Context, uid int64) (
}
// GetAllTagIdsOfAllTransactions returns all transaction tag ids
func (s *TransactionTagService) GetAllTagIdsOfAllTransactions(c *core.Context, uid int64) (map[int64][]int64, error) {
func (s *TransactionTagService) GetAllTagIdsOfAllTransactions(c *core.Context, uid int64) ([]*models.TransactionTagIndex, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var tagIndexs []*models.TransactionTagIndex
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Find(&tagIndexs)
condition := "uid=? AND deleted=?"
conditionParams := make([]any, 0, 2)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
allTransactionTagIds := s.getGroupedTransactionTagIds(tagIndexs)
var allTransactionTagIndexes []*models.TransactionTagIndex
maxTransactionTagIndexId := int64(0)
for maxTransactionTagIndexId >= 0 {
var tagIndexes []*models.TransactionTagIndex
finalCondition := condition
finalConditionParams := make([]any, 0, 3)
finalConditionParams = append(finalConditionParams, conditionParams...)
if maxTransactionTagIndexId > 0 {
finalCondition = finalCondition + " AND tag_index_id<=?"
finalConditionParams = append(finalConditionParams, maxTransactionTagIndexId)
}
err := s.UserDataDB(uid).NewSession(c).Where(finalCondition, finalConditionParams...).Limit(pageCountForLoadAllTransactionTagIndexes, 0).OrderBy("tag_index_id desc").Find(&tagIndexes)
if err != nil {
return nil, err
}
allTransactionTagIndexes = append(allTransactionTagIndexes, tagIndexes...)
if len(tagIndexes) < pageCountForLoadAllTransactionTagIndexes {
maxTransactionTagIndexId = -1
break
}
maxTransactionTagIndexId = tagIndexes[len(tagIndexes)-1].TagIndexId - 1
}
return allTransactionTagIndexes, nil
}
// GetAllTagIdsMapOfAllTransactions returns all transaction tag ids map grouped by transaction id
func (s *TransactionTagService) GetAllTagIdsMapOfAllTransactions(c *core.Context, uid int64) (map[int64][]int64, error) {
tagIndexes, err := s.GetAllTagIdsOfAllTransactions(c, uid)
if err != nil {
return nil, err
}
allTransactionTagIds := s.GetGroupedTransactionTagIds(tagIndexes)
return allTransactionTagIds, err
}
@@ -136,10 +183,10 @@ func (s *TransactionTagService) GetAllTagIdsOfTransactions(c *core.Context, uid
return nil, errs.ErrUserIdInvalid
}
var tagIndexs []*models.TransactionTagIndex
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).Find(&tagIndexs)
var tagIndexes []*models.TransactionTagIndex
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).OrderBy("transaction_id asc, tag_index_id asc").Find(&tagIndexes)
allTransactionTagIds := s.getGroupedTransactionTagIds(tagIndexs)
allTransactionTagIds := s.GetGroupedTransactionTagIds(tagIndexes)
return allTransactionTagIds, err
}
@@ -330,6 +377,32 @@ func (s *TransactionTagService) ExistsTagName(c *core.Context, uid int64, name s
return s.UserDataDB(uid).NewSession(c).Cols("name").Where("uid=? AND deleted=? AND name=?", uid, false, name).Exist(&models.TransactionTag{})
}
// ModifyTagIndexTransactionTime updates transaction time of given transaction tag indexes
func (s *TransactionTagService) ModifyTagIndexTransactionTime(c *core.Context, uid int64, tagIndexes []*models.TransactionTagIndex) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
for i := 0; i < len(tagIndexes); i++ {
tagIndexes[i].UpdatedUnixTime = time.Now().Unix()
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
updatedRows, err := sess.ID(tagIndex.TagIndexId).Cols("transaction_time", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(tagIndex)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTransactionTagIndexNotFound
}
}
return nil
})
}
// GetTagMapByList returns a transaction tag map by a list
func (s *TransactionTagService) GetTagMapByList(tags []*models.TransactionTag) map[int64]*models.TransactionTag {
tagMap := make(map[int64]*models.TransactionTag)
@@ -341,11 +414,11 @@ func (s *TransactionTagService) GetTagMapByList(tags []*models.TransactionTag) m
return tagMap
}
func (s *TransactionTagService) getGroupedTransactionTagIds(tagIndexs []*models.TransactionTagIndex) map[int64][]int64 {
func (s *TransactionTagService) GetGroupedTransactionTagIds(tagIndexes []*models.TransactionTagIndex) map[int64][]int64 {
allTransactionTagIds := make(map[int64][]int64)
for i := 0; i < len(tagIndexs); i++ {
tagIndex := tagIndexs[i]
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
var transactionTagIds []int64
+240
View File
@@ -0,0 +1,240 @@
package services
import (
"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/models"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
// TransactionTemplateService represents transaction template service
type TransactionTemplateService struct {
ServiceUsingDB
ServiceUsingUuid
}
// Initialize a transaction template service singleton instance
var (
TransactionTemplates = &TransactionTemplateService{
ServiceUsingDB: ServiceUsingDB{
container: datastore.Container,
},
ServiceUsingUuid: ServiceUsingUuid{
container: uuid.Container,
},
}
)
// GetTotalNormalTemplateCountByUid returns total template count of user
func (s *TransactionTemplateService) GetTotalNormalTemplateCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND template_type=?", uid, false, models.TRANSACTION_TEMPLATE_TYPE_NORMAL).Count(&models.TransactionTemplate{})
return count, err
}
// GetAllTemplatesByUid returns all transaction template models of user
func (s *TransactionTemplateService) GetAllTemplatesByUid(c *core.Context, uid int64, templateType models.TransactionTemplateType) ([]*models.TransactionTemplate, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var templates []*models.TransactionTemplate
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND template_type=?", uid, false, templateType).Find(&templates)
return templates, err
}
// GetTemplateByTemplateId returns a transaction template model according to transaction template id
func (s *TransactionTemplateService) GetTemplateByTemplateId(c *core.Context, uid int64, templateId int64) (*models.TransactionTemplate, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if templateId <= 0 {
return nil, errs.ErrTransactionTemplateIdInvalid
}
template := &models.TransactionTemplate{}
has, err := s.UserDataDB(uid).NewSession(c).ID(templateId).Where("uid=? AND deleted=?", uid, false).Get(template)
if err != nil {
return nil, err
} else if !has {
return nil, errs.ErrTransactionTemplateNotFound
}
return template, nil
}
// GetMaxDisplayOrder returns the max display order
func (s *TransactionTemplateService) GetMaxDisplayOrder(c *core.Context, uid int64, templateType models.TransactionTemplateType) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
template := &models.TransactionTemplate{}
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=? AND template_type=?", uid, false, templateType).OrderBy("display_order desc").Limit(1).Get(template)
if err != nil {
return 0, err
}
if has {
return template.DisplayOrder, nil
} else {
return 0, nil
}
}
// CreateTemplate saves a new transaction template model to database
func (s *TransactionTemplateService) CreateTemplate(c *core.Context, template *models.TransactionTemplate) error {
if template.Uid <= 0 {
return errs.ErrUserIdInvalid
}
template.TemplateId = s.GenerateUuid(uuid.UUID_TYPE_TEMPLATE)
if template.TemplateId < 1 {
return errs.ErrSystemIsBusy
}
template.Deleted = false
template.CreatedUnixTime = time.Now().Unix()
template.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Insert(template)
return err
})
}
// ModifyTemplate saves an existed transaction template model to database
func (s *TransactionTemplateService) ModifyTemplate(c *core.Context, template *models.TransactionTemplate) error {
if template.Uid <= 0 {
return errs.ErrUserIdInvalid
}
template.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTransactionTemplateNotFound
}
return err
})
}
// HideTemplate updates hidden field of given transaction templates
func (s *TransactionTemplateService) HideTemplate(c *core.Context, uid int64, ids []int64, hidden bool) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.TransactionTemplate{
Hidden: hidden,
UpdatedUnixTime: now,
}
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("template_id", ids).Update(updateModel)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTransactionTemplateNotFound
}
return err
})
}
// ModifyTemplateDisplayOrders updates display order of given transaction templates
func (s *TransactionTemplateService) ModifyTemplateDisplayOrders(c *core.Context, uid int64, templates []*models.TransactionTemplate) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
for i := 0; i < len(templates); i++ {
templates[i].UpdatedUnixTime = time.Now().Unix()
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(templates); i++ {
template := templates[i]
updatedRows, err := sess.ID(template.TemplateId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(template)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTransactionTemplateNotFound
}
}
return nil
})
}
// DeleteTemplate deletes an existed transaction template from database
func (s *TransactionTemplateService) DeleteTemplate(c *core.Context, uid int64, templateId int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.TransactionTemplate{
Deleted: true,
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
deletedRows, err := sess.ID(templateId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
if err != nil {
return err
} else if deletedRows < 1 {
return errs.ErrTransactionTemplateNotFound
}
return err
})
}
// DeleteAllTemplates deletes all existed transaction templates from database
func (s *TransactionTemplateService) DeleteAllTemplates(c *core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.TransactionTemplate{
Deleted: true,
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
if err != nil {
return err
}
return nil
})
}
+535 -139
View File
@@ -5,6 +5,7 @@ import (
"strings"
"time"
"xorm.io/builder"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -15,6 +16,8 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
const pageCountForLoadTransactionAmounts = 1000
// TransactionService represents transaction service
type TransactionService struct {
ServiceUsingDB
@@ -71,11 +74,11 @@ func (s *TransactionService) GetAllTransactions(c *core.Context, uid int64, page
// GetAllTransactionsByMaxTime returns all transactions before given time
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)
return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, count, false, noDuplicated)
}
// GetTransactionsByMaxTime returns transactions before given time
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) {
func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -99,34 +102,44 @@ func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64
actualCount++
}
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, noDuplicated)
err = s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, noDuplicated)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
if len(tagIds) > 0 {
sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds))
} else if noTags {
sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime))
}
err = sess.Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
return transactions, err
}
// 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) {
func (s *TransactionService) GetTransactionsInMonthByPage(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, amountFilter string, keyword string) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
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))
minTransactionTime, maxTransactionTime, err := utils.GetTransactionTimeRangeByYearMonth(year, month)
if err != nil {
return nil, errs.ErrSystemError
}
endMaxUnixTime := startMaxUnixTime.AddDate(0, 1, 0)
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).NewSession(c).Where(condition, conditionParams...).OrderBy("transaction_time desc").Find(&transactions)
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, true)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
if len(tagIds) > 0 {
sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds))
} else if noTags {
sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime))
}
err = sess.OrderBy("transaction_time desc").Find(&transactions)
transactionsInMonth := make([]*models.Transaction, 0, len(transactions))
@@ -167,37 +180,25 @@ func (s *TransactionService) GetTransactionByTransactionId(c *core.Context, uid
// GetAllTransactionCount returns total count of transactions
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(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
}
startTime, err := utils.ParseFromLongDateTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month), utcOffset)
if err != nil {
return 0, errs.ErrSystemError
}
endTime := startTime.AddDate(0, 1, 0)
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startTime.Unix())
maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endTime.Unix()) - 1
return s.GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword)
return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, "", "")
}
// GetTransactionCount returns count of transactions
func (s *TransactionService) GetTransactionCount(c *core.Context, 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, tagIds []int64, noTags bool, amountFilter string, 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).NewSession(c).Where(condition, conditionParams...).Count(&models.Transaction{})
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, true)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
if len(tagIds) > 0 {
sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds))
} else if noTags {
sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime))
}
return sess.Count(&models.Transaction{})
}
// CreateTransaction saves a new transaction to database
@@ -239,7 +240,7 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
transaction.UpdatedUnixTime = now
tagIds = utils.ToUniqueInt64Slice(tagIds)
transactionTagIndexs := make([]*models.TransactionTagIndex, len(tagIds))
transactionTagIndexes := make([]*models.TransactionTagIndex, len(tagIds))
for i := 0; i < len(tagIds); i++ {
tagIndexId := s.GenerateUuid(uuid.UUID_TYPE_TAG_INDEX)
@@ -248,7 +249,7 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
return errs.ErrSystemIsBusy
}
transactionTagIndexs[i] = &models.TransactionTagIndex{
transactionTagIndexes[i] = &models.TransactionTagIndex{
TagIndexId: tagIndexId,
Uid: transaction.Uid,
Deleted: false,
@@ -271,6 +272,10 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
return errs.ErrCannotAddTransactionToHiddenAccount
}
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
return errs.ErrCannotAddTransactionToParentAccount
}
if (transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN) &&
sourceAccount.Currency == destinationAccount.Currency && transaction.Amount != transaction.RelatedAccountAmount {
return errs.ErrTransactionSourceAndDestinationAmountNotEqual
@@ -284,7 +289,7 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
}
// Get and verify tags
err = s.isTagsValid(sess, transaction, transactionTagIndexs, tagIds)
err = s.isTagsValid(sess, transaction, transactionTagIndexes, tagIds)
if err != nil {
return err
@@ -357,9 +362,11 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
err = nil
// Insert transaction tag index
if len(transactionTagIndexs) > 0 {
for i := 0; i < len(transactionTagIndexs); i++ {
transactionTagIndex := transactionTagIndexs[i]
if len(transactionTagIndexes) > 0 {
for i := 0; i < len(transactionTagIndexes); i++ {
transactionTagIndex := transactionTagIndexes[i]
transactionTagIndex.TransactionTime = transaction.TransactionTime
_, err := sess.Insert(transactionTagIndex)
if err != nil {
@@ -423,7 +430,7 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
}
// ModifyTransaction saves an existed transaction to database
func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *models.Transaction, addTagIds []int64, removeTagIds []int64) error {
func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *models.Transaction, currentTagIdsCount int, addTagIds []int64, removeTagIds []int64) error {
if transaction.Uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -439,7 +446,7 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
addTagIds = utils.ToUniqueInt64Slice(addTagIds)
removeTagIds = utils.ToUniqueInt64Slice(removeTagIds)
transactionTagIndexs := make([]*models.TransactionTagIndex, len(addTagIds))
transactionTagIndexes := make([]*models.TransactionTagIndex, len(addTagIds))
for i := 0; i < len(addTagIds); i++ {
tagIndexId := s.GenerateUuid(uuid.UUID_TYPE_TAG_INDEX)
@@ -448,7 +455,7 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
return errs.ErrSystemIsBusy
}
transactionTagIndexs[i] = &models.TransactionTagIndex{
transactionTagIndexes[i] = &models.TransactionTagIndex{
TagIndexId: tagIndexId,
Uid: transaction.Uid,
Deleted: false,
@@ -494,6 +501,10 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
return errs.ErrCannotModifyTransactionInHiddenAccount
}
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
return errs.ErrCannotModifyTransactionInParentAccount
}
if (transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN) &&
sourceAccount.Currency == destinationAccount.Currency && transaction.Amount != transaction.RelatedAccountAmount {
return errs.ErrTransactionSourceAndDestinationAmountNotEqual
@@ -521,6 +532,8 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
updateCols = append(updateCols, "category_id")
}
modifyTransactionTime := false
if utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) != utils.GetUnixTimeFromTransactionTime(oldTransaction.TransactionTime) {
sameSecondLatestTransaction := &models.Transaction{}
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
@@ -539,6 +552,7 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
}
updateCols = append(updateCols, "transaction_time")
modifyTransactionTime = true
}
if transaction.TimezoneUtcOffset != oldTransaction.TimezoneUtcOffset {
@@ -586,7 +600,7 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
}
// Get and verify tags
err = s.isTagsValid(sess, transaction, transactionTagIndexs, addTagIds)
err = s.isTagsValid(sess, transaction, transactionTagIndexes, addTagIds)
if err != nil {
return err
@@ -634,15 +648,27 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
}
}
if len(transactionTagIndexs) > 0 {
for i := 0; i < len(transactionTagIndexs); i++ {
transactionTagIndex := transactionTagIndexs[i]
if len(transactionTagIndexes) > 0 {
for i := 0; i < len(transactionTagIndexes); i++ {
transactionTagIndex := transactionTagIndexes[i]
transactionTagIndex.TransactionTime = transaction.TransactionTime
_, err := sess.Insert(transactionTagIndex)
if err != nil {
return err
}
}
} else if len(transactionTagIndexes) == 0 && currentTagIdsCount > 0 && modifyTransactionTime {
tagIndexUpdateModel := &models.TransactionTagIndex{
TransactionTime: transaction.TransactionTime,
}
_, err := sess.Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).Update(tagIndexUpdateModel)
if err != nil {
return err
}
}
// Update account table
@@ -839,6 +865,10 @@ func (s *TransactionService) DeleteTransaction(c *core.Context, uid int64, trans
return errs.ErrCannotDeleteTransactionInHiddenAccount
}
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
return errs.ErrCannotDeleteTransactionInParentAccount
}
// Update transaction row to deleted
deletedRows, err := sess.ID(oldTransaction.TransactionId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
@@ -1010,46 +1040,28 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *
}
// GetAccountsTotalIncomeAndExpense returns the every accounts total income and expense amount by specific date range
func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, 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, utcOffset int16, useTransactionTimezone bool) (map[int64]int64, map[int64]int64, error) {
if uid <= 0 {
return nil, nil, errs.ErrUserIdInvalid
}
startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
startLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(startUnixTime, clientLocation)
endLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(endUnixTime, clientLocation)
var transactionTotalAmounts []*models.Transaction
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
}
incomeAmounts := make(map[int64]int64)
expenseAmounts := make(map[int64]int64)
for i := 0; i < len(transactionTotalAmounts); i++ {
transactionTotalAmount := transactionTotalAmounts[i]
if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_INCOME {
incomeAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount
} else if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
expenseAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount
}
}
return incomeAmounts, expenseAmounts, nil
}
// GetAccountsMonthTotalIncomeAndExpense returns the every accounts total income and expense amount in month by specific date range
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
}
startUnixTime = utils.GetMinUnixTimeWithSameLocalDateTime(startUnixTime, utcOffset)
endUnixTime = utils.GetMaxUnixTimeWithSameLocalDateTime(endUnixTime, utcOffset)
startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
condition := "uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?"
conditionParams := make([]any, 0, 4)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
minTransactionTime := startTransactionTime
maxTransactionTime := endTransactionTime
var allTransactions []*models.Transaction
@@ -1057,15 +1069,20 @@ func (s *TransactionService) GetAccountsMonthTotalIncomeAndExpense(c *core.Conte
for maxTransactionTime > 0 {
var transactions []*models.Transaction
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)
finalConditionParams := make([]any, 0, 6)
finalConditionParams = append(finalConditionParams, conditionParams...)
finalConditionParams = append(finalConditionParams, minTransactionTime)
finalConditionParams = append(finalConditionParams, maxTransactionTime)
err := s.UserDataDB(uid).NewSession(c).Select("type, account_id, transaction_time, timezone_utc_offset, amount").Where(condition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
if err != nil {
return nil, err
return nil, nil, err
}
allTransactions = append(allTransactions, transactions...)
if len(transactions) < pageCount {
if len(transactions) < pageCountForLoadTransactionAmounts {
maxTransactionTime = 0
break
}
@@ -1073,74 +1090,269 @@ func (s *TransactionService) GetAccountsMonthTotalIncomeAndExpense(c *core.Conte
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
}
totalAmounts := make(map[string]models.TransactionAccountsAmount)
incomeAmounts := make(map[int64]int64)
expenseAmounts := make(map[int64]int64)
for i := 0; i < len(allTransactions); i++ {
transaction := allTransactions[i]
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
yearMonth := utils.FormatUnixTimeToYearMonth(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
timeZone := clientLocation
monthAccountsAmounts, exists := totalAmounts[yearMonth]
if !exists {
monthAccountsAmounts = make(models.TransactionAccountsAmount)
totalAmounts[yearMonth] = monthAccountsAmounts
if useTransactionTimezone {
timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
}
monthAccountAmount, exists := monthAccountsAmounts[transaction.AccountId]
localDateTime := utils.FormatUnixTimeToNumericLocalDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone)
if !exists {
monthAccountAmount = &models.TransactionAccountAmount{
AccountId: transaction.AccountId,
TotalIncomeAmount: 0,
TotalExpenseAmount: 0,
}
monthAccountsAmounts[transaction.AccountId] = monthAccountAmount
if localDateTime < startLocalDateTime || localDateTime > endLocalDateTime {
continue
}
var amountsMap map[int64]int64
if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
monthAccountAmount.TotalIncomeAmount += transaction.Amount
amountsMap = incomeAmounts
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
monthAccountAmount.TotalExpenseAmount += transaction.Amount
amountsMap = expenseAmounts
}
totalAmounts, exists := amountsMap[transaction.AccountId]
if !exists {
totalAmounts = 0
}
totalAmounts += transaction.Amount
amountsMap[transaction.AccountId] = totalAmounts
}
return totalAmounts, nil
return incomeAmounts, expenseAmounts, nil
}
// GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) ([]*models.Transaction, error) {
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
var startLocalDateTime, endLocalDateTime, startTransactionTime, endTransactionTime int64
if startUnixTime > 0 {
startLocalDateTime = utils.FormatUnixTimeToNumericLocalDateTime(startUnixTime, clientLocation)
startUnixTime = utils.GetMinUnixTimeWithSameLocalDateTime(startUnixTime, utcOffset)
startTransactionTime = utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
}
if endUnixTime > 0 {
endLocalDateTime = utils.FormatUnixTimeToNumericLocalDateTime(endUnixTime, clientLocation)
endUnixTime = utils.GetMaxUnixTimeWithSameLocalDateTime(endUnixTime, utcOffset)
endTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
}
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
conditionParams := make([]any, 0, 8)
conditionParams := make([]any, 0, 4)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
if startUnixTime > 0 {
condition = condition + " AND transaction_time>=?"
conditionParams = append(conditionParams, utils.GetMinTransactionTimeFromUnixTime(startUnixTime))
minTransactionTime := startTransactionTime
maxTransactionTime := endTransactionTime
var allTransactions []*models.Transaction
for maxTransactionTime >= 0 {
var transactions []*models.Transaction
finalCondition := condition
finalConditionParams := make([]any, 0, 6)
finalConditionParams = append(finalConditionParams, conditionParams...)
if minTransactionTime > 0 {
finalCondition = finalCondition + " AND transaction_time>=?"
finalConditionParams = append(finalConditionParams, minTransactionTime)
}
if maxTransactionTime > 0 {
finalCondition = finalCondition + " AND transaction_time<=?"
finalConditionParams = append(finalConditionParams, maxTransactionTime)
}
err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
if err != nil {
return nil, err
}
allTransactions = append(allTransactions, transactions...)
if len(transactions) < pageCountForLoadTransactionAmounts {
maxTransactionTime = -1
break
}
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
}
if endUnixTime > 0 {
condition = condition + " AND transaction_time<=?"
conditionParams = append(conditionParams, utils.GetMaxTransactionTimeFromUnixTime(endUnixTime))
transactionTotalAmountsMap := make(map[string]*models.Transaction)
for i := 0; i < len(allTransactions); i++ {
transaction := allTransactions[i]
timeZone := clientLocation
if useTransactionTimezone {
timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
}
localDateTime := utils.FormatUnixTimeToNumericLocalDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone)
if (startLocalDateTime > 0 && localDateTime < startLocalDateTime) || (endLocalDateTime > 0 && localDateTime > endLocalDateTime) {
continue
}
groupKey := fmt.Sprintf("%d_%d", transaction.CategoryId, transaction.AccountId)
totalAmounts, exists := transactionTotalAmountsMap[groupKey]
if !exists {
totalAmounts = &models.Transaction{
CategoryId: transaction.CategoryId,
AccountId: transaction.AccountId,
Amount: 0,
}
transactionTotalAmountsMap[groupKey] = totalAmounts
}
totalAmounts.Amount += transaction.Amount
}
var transactionTotalAmounts []*models.Transaction
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)
transactionTotalAmounts := make([]*models.Transaction, 0, len(transactionTotalAmountsMap))
if err != nil {
return nil, err
for _, totalAmounts := range transactionTotalAmountsMap {
transactionTotalAmounts = append(transactionTotalAmounts, totalAmounts)
}
return transactionTotalAmounts, nil
}
// GetAccountsAndCategoriesMonthlyIncomeAndExpense returns the every accounts monthly income and expense amount by specific date range
func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c *core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
var startTransactionTime, endTransactionTime int64
var err error
if startYear > 0 && startMonth > 0 {
startTransactionTime, _, err = utils.GetTransactionTimeRangeByYearMonth(startYear, startMonth)
if err != nil {
return nil, errs.ErrSystemError
}
}
if endYear > 0 && endMonth > 0 {
_, endTransactionTime, err = utils.GetTransactionTimeRangeByYearMonth(endYear, endMonth)
if err != nil {
return nil, errs.ErrSystemError
}
}
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
conditionParams := make([]any, 0, 4)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
minTransactionTime := startTransactionTime
maxTransactionTime := endTransactionTime
var allTransactions []*models.Transaction
for maxTransactionTime >= 0 {
var transactions []*models.Transaction
finalCondition := condition
finalConditionParams := make([]any, 0, 6)
finalConditionParams = append(finalConditionParams, conditionParams...)
if minTransactionTime > 0 {
finalCondition = finalCondition + " AND transaction_time>=?"
finalConditionParams = append(finalConditionParams, minTransactionTime)
}
if maxTransactionTime > 0 {
finalCondition = finalCondition + " AND transaction_time<=?"
finalConditionParams = append(finalConditionParams, maxTransactionTime)
}
err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
if err != nil {
return nil, err
}
allTransactions = append(allTransactions, transactions...)
if len(transactions) < pageCountForLoadTransactionAmounts {
maxTransactionTime = -1
break
}
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
}
startYearMonth := startYear*100 + startMonth
endYearMonth := endYear*100 + endMonth
transactionsMonthlyAmountsMap := make(map[string]*models.Transaction)
transactionsMonthlyAmounts := make(map[int32][]*models.Transaction)
for i := 0; i < len(allTransactions); i++ {
transaction := allTransactions[i]
timeZone := clientLocation
if useTransactionTimezone {
timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
}
yearMonth := utils.FormatUnixTimeToNumericYearMonth(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone)
if (startYearMonth > 0 && yearMonth < startYearMonth) || (endYearMonth > 0 && yearMonth > endYearMonth) {
continue
}
groupKey := fmt.Sprintf("%d_%d_%d", yearMonth, transaction.CategoryId, transaction.AccountId)
transactionAmounts, exists := transactionsMonthlyAmountsMap[groupKey]
if !exists {
transactionAmounts = &models.Transaction{
CategoryId: transaction.CategoryId,
AccountId: transaction.AccountId,
}
transactionsMonthlyAmountsMap[groupKey] = transactionAmounts
}
transactionAmounts.Amount += transaction.Amount
}
for groupKey, transaction := range transactionsMonthlyAmountsMap {
groupKeyParts := strings.Split(groupKey, "_")
yearMonth, _ := utils.StringToInt32(groupKeyParts[0])
monthlyAmounts, exists := transactionsMonthlyAmounts[yearMonth]
if !exists {
monthlyAmounts = make([]*models.Transaction, 0, 0)
}
monthlyAmounts = append(monthlyAmounts, transaction)
transactionsMonthlyAmounts[yearMonth] = monthlyAmounts
}
return transactionsMonthlyAmounts, nil
}
// GetTransactionMapByList returns a transaction map by a list
func (s *TransactionService) GetTransactionMapByList(transactions []*models.Transaction) map[int64]*models.Transaction {
transactionMap := make(map[int64]*models.Transaction)
@@ -1153,7 +1365,7 @@ 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, []any) {
func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, amountFilter string, keyword string, noDuplicated bool) (string, []any) {
condition := "uid=? AND deleted=?"
conditionParams := make([]any, 0, 16)
conditionParams = append(conditionParams, uid)
@@ -1169,6 +1381,18 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
conditionParams = append(conditionParams, minTransactionTime)
}
var accountIdsCondition strings.Builder
accountIdConditionParams := make([]any, 0, len(accountIds))
for i := 0; i < len(accountIds); i++ {
if i > 0 {
accountIdsCondition.WriteString(",")
}
accountIdsCondition.WriteString("?")
accountIdConditionParams = append(accountIdConditionParams, accountIds[i])
}
if models.TRANSACTION_DB_TYPE_MODIFY_BALANCE <= transactionType && transactionType <= models.TRANSACTION_DB_TYPE_EXPENSE {
condition = condition + " AND type=?"
conditionParams = append(conditionParams, transactionType)
@@ -1176,18 +1400,35 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
if len(accountIds) == 0 {
condition = condition + " AND type=?"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
} else {
} else if len(accountIds) == 1 {
condition = condition + " AND (type=? OR type=?)"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN)
} else { // len(accountsIds) > 1
condition = condition + " AND (type=? OR (type=? AND related_account_id NOT IN (" + accountIdsCondition.String() + ")))"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN)
conditionParams = append(conditionParams, accountIdConditionParams...)
}
} else {
if noDuplicated && len(accountIds) == 0 {
condition = condition + " AND (type=? OR type=? OR type=? OR type=?)"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
if noDuplicated {
if len(accountIds) == 0 {
condition = condition + " AND (type=? OR type=? OR type=? OR type=?)"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
} else if len(accountIds) == 1 {
// Do Nothing
} else { // len(accountsIds) > 1
condition = condition + " AND (type=? OR type=? OR type=? OR type=? OR (type=? AND related_account_id NOT IN (" + accountIdsCondition.String() + ")))"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN)
conditionParams = append(conditionParams, accountIdConditionParams...)
}
}
}
@@ -1203,22 +1444,73 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
conditionParams = append(conditionParams, categoryIds[i])
}
condition = condition + " AND category_id IN (" + conditions.String() + ")"
if conditions.Len() > 1 {
condition = condition + " AND category_id IN (" + conditions.String() + ")"
} else {
condition = condition + " AND category_id = " + conditions.String()
}
}
if len(accountIds) > 0 {
var conditions strings.Builder
for i := 0; i < len(accountIds); i++ {
if i > 0 {
conditions.WriteString(",")
}
conditions.WriteString("?")
conditionParams = append(conditionParams, accountIds[i])
if accountIdsCondition.Len() > 1 {
condition = condition + " AND account_id IN (" + accountIdsCondition.String() + ")"
} else {
condition = condition + " AND account_id = " + accountIdsCondition.String()
}
condition = condition + " AND account_id IN (" + conditions.String() + ")"
conditionParams = append(conditionParams, accountIdConditionParams...)
}
if amountFilter != "" {
amountFilterItems := strings.Split(amountFilter, ":")
if len(amountFilterItems) == 2 && amountFilterItems[0] == "gt" {
value, err := utils.StringToInt64(amountFilterItems[1])
if err == nil {
condition = condition + " AND amount > ?"
conditionParams = append(conditionParams, value)
}
} else if len(amountFilterItems) == 2 && amountFilterItems[0] == "lt" {
value, err := utils.StringToInt64(amountFilterItems[1])
if err == nil {
condition = condition + " AND amount < ?"
conditionParams = append(conditionParams, value)
}
} else if len(amountFilterItems) == 2 && amountFilterItems[0] == "eq" {
value, err := utils.StringToInt64(amountFilterItems[1])
if err == nil {
condition = condition + " AND amount = ?"
conditionParams = append(conditionParams, value)
}
} else if len(amountFilterItems) == 2 && amountFilterItems[0] == "ne" {
value, err := utils.StringToInt64(amountFilterItems[1])
if err == nil {
condition = condition + " AND amount <> ?"
conditionParams = append(conditionParams, value)
}
} else if len(amountFilterItems) == 3 && amountFilterItems[0] == "bt" {
value1, err := utils.StringToInt64(amountFilterItems[1])
value2, err := utils.StringToInt64(amountFilterItems[2])
if err == nil {
condition = condition + " AND amount >= ? AND amount <= ?"
conditionParams = append(conditionParams, value1)
conditionParams = append(conditionParams, value2)
}
} else if len(amountFilterItems) == 3 && amountFilterItems[0] == "nb" {
value1, err := utils.StringToInt64(amountFilterItems[1])
value2, err := utils.StringToInt64(amountFilterItems[2])
if err == nil {
condition = condition + " AND (amount < ? OR amount > ?)"
conditionParams = append(conditionParams, value1)
conditionParams = append(conditionParams, value2)
}
}
}
if keyword != "" {
@@ -1229,6 +1521,40 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
return condition, conditionParams
}
func (s *TransactionService) getTransactionQueryByTagIdsCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, tagIds []int64) *builder.Builder {
if len(tagIds) > 0 {
condition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false})
if maxTransactionTime > 0 {
condition = condition.And(builder.Lte{"transaction_time": maxTransactionTime})
}
if minTransactionTime > 0 {
condition = condition.And(builder.Gte{"transaction_time": minTransactionTime})
}
condition = condition.And(builder.In("tag_id", tagIds))
return builder.Select("transaction_id").From("transaction_tag_index").Where(condition)
}
return nil
}
func (s *TransactionService) getTransactionQueryByAllTagIdsCondition(uid int64, maxTransactionTime int64, minTransactionTime int64) *builder.Builder {
condition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false})
if maxTransactionTime > 0 {
condition = condition.And(builder.Lte{"transaction_time": maxTransactionTime})
}
if minTransactionTime > 0 {
condition = condition.And(builder.Gte{"transaction_time": minTransactionTime})
}
return builder.Select("transaction_id").From("transaction_tag_index").Where(condition)
}
func (s *TransactionService) isAccountIdValid(transaction *models.Transaction) error {
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if transaction.RelatedAccountId != 0 && transaction.RelatedAccountId != transaction.AccountId {
@@ -1266,6 +1592,7 @@ func (s *TransactionService) getAccountModels(sess *xorm.Session, transaction *m
return nil, nil, errs.ErrSourceAccountNotFound
}
// check whether the related account is valid
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if transaction.RelatedAccountId != 0 && transaction.RelatedAccountId != transaction.AccountId {
return nil, nil, errs.ErrAccountIdInvalid
@@ -1292,6 +1619,54 @@ func (s *TransactionService) getAccountModels(sess *xorm.Session, transaction *m
}
}
// check whether the parent accounts are valid
if sourceAccount.ParentAccountId > 0 && destinationAccount != nil && sourceAccount.ParentAccountId != destinationAccount.ParentAccountId && destinationAccount.ParentAccountId > 0 {
var accounts []*models.Account
err := sess.Where("uid=? AND deleted=? and (account_id=? or account_id=?)", transaction.Uid, false, sourceAccount.ParentAccountId, destinationAccount.ParentAccountId).Find(&accounts)
if err != nil {
return nil, nil, err
}
if len(accounts) < 2 {
return nil, nil, errs.ErrAccountNotFound
}
for i := 0; i < len(accounts); i++ {
account := accounts[i]
if account.Hidden {
return nil, nil, errs.ErrCannotUseHiddenAccount
}
}
} else if sourceAccount.ParentAccountId > 0 && (destinationAccount == nil || sourceAccount.ParentAccountId == destinationAccount.ParentAccountId || destinationAccount.ParentAccountId == 0) {
sourceParentAccount := &models.Account{}
has, err = sess.ID(sourceAccount.ParentAccountId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(sourceParentAccount)
if err != nil {
return nil, nil, err
} else if !has {
return nil, nil, errs.ErrSourceAccountNotFound
}
if sourceParentAccount.Hidden {
return nil, nil, errs.ErrCannotUseHiddenAccount
}
} else if sourceAccount.ParentAccountId == 0 && destinationAccount != nil && destinationAccount.ParentAccountId > 0 {
destinationParentAccount := &models.Account{}
has, err = sess.ID(destinationAccount.ParentAccountId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(destinationParentAccount)
if err != nil {
return nil, nil, err
} else if !has {
return nil, nil, errs.ErrDestinationAccountNotFound
}
if destinationParentAccount.Hidden {
return nil, nil, errs.ErrCannotUseHiddenAccount
}
}
return sourceAccount, destinationAccount, nil
}
@@ -1360,6 +1735,10 @@ func (s *TransactionService) isCategoryValid(sess *xorm.Session, transaction *mo
return errs.ErrTransactionCategoryNotFound
}
if category.Hidden {
return errs.ErrCannotUseHiddenTransactionCategory
}
if category.ParentCategoryId < 1 {
return errs.ErrCannotUsePrimaryCategoryForTransaction
}
@@ -1369,13 +1748,26 @@ func (s *TransactionService) isCategoryValid(sess *xorm.Session, transaction *mo
((transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN) && category.Type != models.CATEGORY_TYPE_TRANSFER) {
return errs.ErrTransactionCategoryTypeInvalid
}
parentCategory := &models.TransactionCategory{}
has, err = sess.ID(category.ParentCategoryId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(parentCategory)
if err != nil {
return err
} else if !has {
return errs.ErrTransactionCategoryNotFound
}
if parentCategory.Hidden {
return errs.ErrCannotUseHiddenTransactionCategory
}
}
return nil
}
func (s *TransactionService) isTagsValid(sess *xorm.Session, transaction *models.Transaction, transactionTagIndexs []*models.TransactionTagIndex, tagIds []int64) error {
if len(transactionTagIndexs) > 0 {
func (s *TransactionService) isTagsValid(sess *xorm.Session, transaction *models.Transaction, transactionTagIndexes []*models.TransactionTagIndex, tagIds []int64) error {
if len(transactionTagIndexes) > 0 {
var tags []*models.TransactionTag
err := sess.Where("uid=? AND deleted=?", transaction.Uid, false).In("tag_id", tagIds).Find(&tags)
@@ -1386,11 +1778,15 @@ func (s *TransactionService) isTagsValid(sess *xorm.Session, transaction *models
tagMap := make(map[int64]*models.TransactionTag)
for i := 0; i < len(tags); i++ {
if tags[i].Hidden {
return errs.ErrCannotUseHiddenTransactionTag
}
tagMap[tags[i].TagId] = tags[i]
}
for i := 0; i < len(transactionTagIndexs); i++ {
if _, exists := tagMap[transactionTagIndexs[i].TagId]; !exists {
for i := 0; i < len(transactionTagIndexes); i++ {
if _, exists := tagMap[transactionTagIndexes[i].TagId]; !exists {
return errs.ErrTransactionTagNotFound
}
}
+43
View File
@@ -248,6 +248,30 @@ func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserL
updateCols = append(updateCols, "short_time_format")
}
if core.DECIMAL_SEPARATOR_DEFAULT <= user.DecimalSeparator && user.DecimalSeparator <= core.DECIMAL_SEPARATOR_SPACE {
updateCols = append(updateCols, "decimal_separator")
}
if core.DIGIT_GROUPING_SYMBOL_DEFAULT <= user.DigitGroupingSymbol && user.DigitGroupingSymbol <= core.DIGIT_GROUPING_SYMBOL_APOSTROPHE {
updateCols = append(updateCols, "digit_grouping_symbol")
}
if core.DIGIT_GROUPING_TYPE_DEFAULT <= user.DigitGrouping && user.DigitGrouping <= core.DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR {
updateCols = append(updateCols, "digit_grouping")
}
if models.CURRENCY_DISPLAY_TYPE_DEFAULT <= user.CurrencyDisplayType && user.CurrencyDisplayType <= models.CURRENCY_DISPLAY_TYPE_NAME_AFTER_AMOUNT {
updateCols = append(updateCols, "currency_display_type")
}
if models.AMOUNT_COLOR_TYPE_DEFAULT <= user.ExpenseAmountColor && user.ExpenseAmountColor <= models.AMOUNT_COLOR_TYPE_BLACK_OR_WHITE {
updateCols = append(updateCols, "expense_amount_color")
}
if models.AMOUNT_COLOR_TYPE_DEFAULT <= user.IncomeAmountColor && user.IncomeAmountColor <= models.AMOUNT_COLOR_TYPE_BLACK_OR_WHITE {
updateCols = append(updateCols, "income_amount_color")
}
user.UpdatedUnixTime = now
updateCols = append(updateCols, "updated_unix_time")
@@ -270,6 +294,25 @@ func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserL
return keyProfileUpdated, emailSetToUnverified, nil
}
// UpdateUserAvatar updated the custom avatar type of specified user
func (s *UserService) UpdateUserAvatar(c *core.Context, uid int64, customAvatarType string) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.User{
CustomAvatarType: customAvatarType,
UpdatedUnixTime: now,
}
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.ID(uid).Cols("custom_avatar_type", "updated_unix_time").Where("deleted=?", false).Update(updateModel)
return err
})
}
// UpdateUserLastLoginTime updates the last login time field
func (s *UserService) UpdateUserLastLoginTime(c *core.Context, uid int64) error {
if uid <= 0 {
+321 -31
View File
@@ -11,6 +11,7 @@ import (
"gopkg.in/ini.v1"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
)
const (
@@ -57,14 +58,26 @@ const (
Sqlite3DbType string = "sqlite3"
)
// Object Storage types
const (
LocalFileSystemObjectStorageType string = "local_filesystem"
MinIOStorageType string = "minio"
)
// Uuid generator types
const (
InternalUuidGeneratorType string = "internal"
)
// Duplicate checker types
const (
InMemoryDuplicateCheckerType string = "in_memory"
)
// User avatar provider types
const (
GravatarProvider string = "gravatar"
InternalAvatarProvider string = "internal"
GravatarProvider string = "gravatar"
)
// Map provider types
@@ -74,10 +87,13 @@ const (
OpenTopoMapProvider string = "opentopomap"
OPNVKarteMapProvider string = "opnvkarte"
CyclOSMMapProvider string = "cyclosm"
GoogleMapProvider string = "googlemap"
CartoDBMapProvider string = "cartodb"
TomTomMapProvider string = "tomtom"
TianDiTuProvider string = "tianditu"
GoogleMapProvider string = "googlemap"
BaiduMapProvider string = "baidumap"
AmapProvider string = "amap"
CustomProvider string = "custom"
)
// Amap security verification method
@@ -110,14 +126,19 @@ const (
defaultDatabaseMaxOpenConn uint16 = 0
defaultDatabaseConnMaxLifetime uint32 = 14400
defaultLogMode string = "console"
defaultLoglevel Level = LOGLEVEL_INFO
defaultLogMode string = "console"
defaultLogFileMaxSize uint32 = 104857600 // 100 MB
defaultLogFileMaxDays uint32 = 7 // days
defaultInMemoryDuplicateCheckerCleanupInterval uint32 = 60 // 1 minutes
defaultDuplicateSubmissionsInterval 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
defaultTokenExpiredTime uint32 = 2592000 // 30 days
defaultTokenMinRefreshInterval uint32 = 86400 // 1 day
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes
defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes
defaultExchangeRatesDataRequestTimeout uint32 = 10000 // 10 seconds
)
@@ -148,6 +169,25 @@ type SMTPConfig struct {
FromAddress string
}
// MinIOConfig represents the MinIO setting config
type MinIOConfig struct {
Endpoint string
Location string
AccessKeyID string
SecretAccessKey string
UseSSL bool
SkipTLSVerify bool
Bucket string
RootPath string
}
// NotificationConfig represents a notification setting config
type NotificationConfig struct {
Enabled bool
DefaultContent string
MultiLanguageContent map[string]string
}
// Config represents the global setting config
type Config struct {
// Global
@@ -187,18 +227,38 @@ type Config struct {
EnableConsoleLog bool
EnableFileLog bool
LogLevel Level
FileLogPath string
LogLevel Level
FileLogPath string
RequestFileLogPath string
QueryFileLogPath string
LogFileRotate bool
LogFileMaxSize uint32
LogFileMaxDays uint32
// Storage
StorageType string
LocalFileSystemPath string
MinIOConfig *MinIOConfig
// Uuid
UuidGeneratorType string
UuidServerId uint8
// Duplicate Checker
DuplicateCheckerType string
InMemoryDuplicateCheckerCleanupInterval uint32
InMemoryDuplicateCheckerCleanupIntervalDuration time.Duration
EnableDuplicateSubmissionsCheck bool
DuplicateSubmissionsInterval uint32
DuplicateSubmissionsIntervalDuration time.Duration
// Secret
SecretKeyNoSet bool
SecretKey string
EnableTwoFactor bool
TokenExpiredTime uint32
TokenExpiredTimeDuration time.Duration
TokenMinRefreshInterval uint32
TemporaryTokenExpiredTime uint32
TemporaryTokenExpiredTimeDuration time.Duration
EmailVerifyTokenExpiredTime uint32
@@ -218,20 +278,33 @@ type Config struct {
// Data
EnableDataExport bool
// Notification
AfterRegisterNotification NotificationConfig
AfterLoginNotification NotificationConfig
AfterOpenNotification NotificationConfig
// Map
MapProvider string
TomTomMapAPIKey string
GoogleMapAPIKey string
BaiduMapAK string
AmapApplicationKey string
AmapSecurityVerificationMethod string
AmapApplicationSecret string
AmapApiExternalProxyUrl string
EnableMapDataFetchProxy bool
MapProvider string
EnableMapDataFetchProxy bool
MapProxy string
TomTomMapAPIKey string
TianDiTuAPIKey string
GoogleMapAPIKey string
BaiduMapAK string
AmapApplicationKey string
AmapSecurityVerificationMethod string
AmapApplicationSecret string
AmapApiExternalProxyUrl string
CustomMapTileServerTileLayerUrl string
CustomMapTileServerAnnotationLayerUrl string
CustomMapTileServerMinZoomLevel uint8
CustomMapTileServerMaxZoomLevel uint8
CustomMapTileServerDefaultZoomLevel uint8
// Exchange Rates
ExchangeRatesDataSource string
ExchangeRatesRequestTimeout uint32
ExchangeRatesProxy string
ExchangeRatesSkipTLSVerify bool
}
@@ -282,12 +355,24 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
return nil, err
}
err = loadStorageConfiguration(config, cfgFile, "storage")
if err != nil {
return nil, err
}
err = loadUuidConfiguration(config, cfgFile, "uuid")
if err != nil {
return nil, err
}
err = loadDuplicateCheckerConfiguration(config, cfgFile, "duplicate_checker")
if err != nil {
return nil, err
}
err = loadSecurityConfiguration(config, cfgFile, "security")
if err != nil {
@@ -306,6 +391,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
return nil, err
}
err = loadNotificationConfiguration(config, cfgFile, "notification")
if err != nil {
return nil, err
}
err = loadMapConfiguration(config, cfgFile, "map")
if err != nil {
@@ -341,10 +432,13 @@ func GetDefaultConfigFilePath() (string, error) {
func loadGlobalConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.AppName = getConfigItemStringValue(configFile, sectionName, "app_name", defaultAppName)
config.Mode = MODE_PRODUCTION
if getConfigItemStringValue(configFile, sectionName, "mode") == "development" {
if getConfigItemStringValue(configFile, sectionName, "mode") == "production" {
config.Mode = MODE_PRODUCTION
} else if getConfigItemStringValue(configFile, sectionName, "mode") == "development" {
config.Mode = MODE_DEVELOPMENT
} else {
return errs.ErrInvalidServerMode
}
return nil
@@ -398,6 +492,13 @@ func loadDatabaseConfiguration(config *Config, configFile *ini.File, sectionName
dbConfig := &DatabaseConfig{}
dbConfig.DatabaseType = getConfigItemStringValue(configFile, sectionName, "type", MySqlDbType)
if dbConfig.DatabaseType != MySqlDbType &&
dbConfig.DatabaseType != PostgresDbType &&
dbConfig.DatabaseType != Sqlite3DbType {
return errs.ErrDatabaseTypeInvalid
}
dbConfig.DatabaseHost = getConfigItemStringValue(configFile, sectionName, "host", defaultDatabaseHost)
dbConfig.DatabaseName = getConfigItemStringValue(configFile, sectionName, "name", defaultDatabaseName)
dbConfig.DatabaseUser = getConfigItemStringValue(configFile, sectionName, "user")
@@ -455,17 +556,83 @@ func loadLogConfiguration(config *Config, configFile *ini.File, sectionName stri
}
}
config.LogLevel = getLogLevel(getConfigItemStringValue(configFile, sectionName, "level"), defaultLoglevel)
var err error
config.LogLevel, err = getLogLevel(getConfigItemStringValue(configFile, sectionName, "level"))
if err != nil {
return err
}
if config.LogLevel != LOGLEVEL_DEBUG &&
config.LogLevel != LOGLEVEL_INFO &&
config.LogLevel != LOGLEVEL_WARN &&
config.LogLevel != LOGLEVEL_ERROR {
return errs.ErrInvalidLogLevel
}
if config.EnableFileLog {
fileLogPath := getConfigItemStringValue(configFile, sectionName, "log_path")
finalFileLogPath, _ := getFinalPath(config.WorkingPath, fileLogPath)
config.FileLogPath = finalFileLogPath
requestFileLogPath := getConfigItemStringValue(configFile, sectionName, "request_log_path")
if requestFileLogPath != "" {
finalRequestFileLogPath, _ := getFinalPath(config.WorkingPath, requestFileLogPath)
config.RequestFileLogPath = finalRequestFileLogPath
} else {
config.RequestFileLogPath = ""
}
queryFileLogPath := getConfigItemStringValue(configFile, sectionName, "query_log_path")
if queryFileLogPath != "" {
finalQueryFileLogPath, _ := getFinalPath(config.WorkingPath, queryFileLogPath)
config.QueryFileLogPath = finalQueryFileLogPath
} else {
config.QueryFileLogPath = ""
}
config.LogFileRotate = getConfigItemBoolValue(configFile, sectionName, "log_file_rotate", false)
config.LogFileMaxSize = getConfigItemUint32Value(configFile, sectionName, "log_file_max_size", defaultLogFileMaxSize)
config.LogFileMaxDays = getConfigItemUint32Value(configFile, sectionName, "log_file_max_days", defaultLogFileMaxDays)
}
return nil
}
func loadStorageConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "type") == LocalFileSystemObjectStorageType {
config.StorageType = LocalFileSystemObjectStorageType
} else if getConfigItemStringValue(configFile, sectionName, "type") == MinIOStorageType {
config.StorageType = MinIOStorageType
} else {
return errs.ErrInvalidStorageType
}
localFileSystemRootPath := getConfigItemStringValue(configFile, sectionName, "local_filesystem_path")
finalLocalFileSystemRootPath, err := getFinalPath(config.WorkingPath, localFileSystemRootPath)
config.LocalFileSystemPath = finalLocalFileSystemRootPath
if config.StorageType == LocalFileSystemObjectStorageType && err != nil {
return errs.ErrInvalidLocalFileSystemStoragePath
}
minIOConfig := &MinIOConfig{}
minIOConfig.Endpoint = getConfigItemStringValue(configFile, sectionName, "minio_endpoint")
minIOConfig.Location = getConfigItemStringValue(configFile, sectionName, "minio_location")
minIOConfig.AccessKeyID = getConfigItemStringValue(configFile, sectionName, "minio_access_key_id")
minIOConfig.SecretAccessKey = getConfigItemStringValue(configFile, sectionName, "minio_secret_access_key")
minIOConfig.UseSSL = getConfigItemBoolValue(configFile, sectionName, "minio_use_ssl", false)
minIOConfig.SkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "minio_skip_tls_verify", false)
minIOConfig.Bucket = getConfigItemStringValue(configFile, sectionName, "minio_bucket")
minIOConfig.RootPath = getConfigItemStringValue(configFile, sectionName, "minio_root_path")
config.MinIOConfig = minIOConfig
return nil
}
func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "generator_type") == InternalUuidGeneratorType {
config.UuidGeneratorType = InternalUuidGeneratorType
@@ -478,20 +645,76 @@ func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName str
return nil
}
func loadDuplicateCheckerConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "checker_type") == InMemoryDuplicateCheckerType {
config.DuplicateCheckerType = InMemoryDuplicateCheckerType
} else {
return errs.ErrInvalidDuplicateCheckerType
}
config.InMemoryDuplicateCheckerCleanupInterval = getConfigItemUint32Value(configFile, sectionName, "cleanup_interval", defaultInMemoryDuplicateCheckerCleanupInterval)
if config.InMemoryDuplicateCheckerCleanupInterval < 1 {
return errs.ErrInvalidInMemoryDuplicateCheckerCleanupInterval
}
config.InMemoryDuplicateCheckerCleanupIntervalDuration = time.Duration(config.InMemoryDuplicateCheckerCleanupInterval) * time.Second
duplicateSubmissionsInterval := getConfigItemUint32Value(configFile, sectionName, "duplicate_submissions_interval", defaultDuplicateSubmissionsInterval)
config.EnableDuplicateSubmissionsCheck = duplicateSubmissionsInterval > 0
if duplicateSubmissionsInterval < 1 {
duplicateSubmissionsInterval = defaultDuplicateSubmissionsInterval
}
config.DuplicateSubmissionsInterval = duplicateSubmissionsInterval
config.DuplicateSubmissionsIntervalDuration = time.Duration(config.DuplicateSubmissionsInterval) * time.Second
return nil
}
func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.SecretKeyNoSet = !getConfigItemIsSet(configFile, sectionName, "secret_key")
config.SecretKey = getConfigItemStringValue(configFile, sectionName, "secret_key", defaultSecretKey)
config.EnableTwoFactor = getConfigItemBoolValue(configFile, sectionName, "enable_two_factor", true)
config.TokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "token_expired_time", defaultTokenExpiredTime)
if config.TokenExpiredTime < 60 {
return errs.ErrInvalidTokenExpiredTime
}
config.TokenExpiredTimeDuration = time.Duration(config.TokenExpiredTime) * time.Second
config.TokenMinRefreshInterval = getConfigItemUint32Value(configFile, sectionName, "token_min_refresh_interval", defaultTokenMinRefreshInterval)
if config.TokenMinRefreshInterval >= config.TokenExpiredTime {
return errs.ErrInvalidTokenMinRefreshInterval
}
config.TemporaryTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "temporary_token_expired_time", defaultTemporaryTokenExpiredTime)
if config.TemporaryTokenExpiredTime < 60 {
return errs.ErrInvalidTemporaryTokenExpiredTime
}
config.TemporaryTokenExpiredTimeDuration = time.Duration(config.TemporaryTokenExpiredTime) * time.Second
config.EmailVerifyTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "email_verify_token_expired_time", defaultEmailVerifyTokenExpiredTime)
if config.EmailVerifyTokenExpiredTime < 60 {
return errs.ErrInvalidEmailVerifyTokenExpiredTime
}
config.EmailVerifyTokenExpiredTimeDuration = time.Duration(config.EmailVerifyTokenExpiredTime) * time.Second
config.PasswordResetTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "password_reset_token_expired_time", defaultPasswordResetTokenExpiredTime)
if config.PasswordResetTokenExpiredTime < 60 {
return errs.ErrInvalidPasswordResetTokenExpiredTime
}
config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second
config.EnableRequestIdHeader = getConfigItemBoolValue(configFile, sectionName, "request_id_header", true)
@@ -506,10 +729,14 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str
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 = ""
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == InternalAvatarProvider {
config.AvatarProvider = InternalAvatarProvider
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == GravatarProvider {
config.AvatarProvider = GravatarProvider
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
config.AvatarProvider = ""
} else {
return errs.ErrInvalidAvatarProvider
}
return nil
@@ -521,6 +748,14 @@ func loadDataConfiguration(config *Config, configFile *ini.File, sectionName str
return nil
}
func loadNotificationConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.AfterRegisterNotification = getNotificationConfiguration(configFile, sectionName, "enable_notification_after_register", "after_register_notification_content")
config.AfterLoginNotification = getNotificationConfiguration(configFile, sectionName, "enable_notification_after_login", "after_login_notification_content")
config.AfterOpenNotification = getNotificationConfiguration(configFile, sectionName, "enable_notification_after_open", "after_open_notification_content")
return nil
}
func loadMapConfiguration(config *Config, configFile *ini.File, sectionName string) error {
mapProvider := getConfigItemStringValue(configFile, sectionName, "map_provider")
@@ -536,20 +771,28 @@ func loadMapConfiguration(config *Config, configFile *ini.File, sectionName stri
config.MapProvider = OPNVKarteMapProvider
} else if mapProvider == CyclOSMMapProvider {
config.MapProvider = CyclOSMMapProvider
} else if mapProvider == GoogleMapProvider {
config.MapProvider = GoogleMapProvider
} else if mapProvider == CartoDBMapProvider {
config.MapProvider = CartoDBMapProvider
} else if mapProvider == TomTomMapProvider {
config.MapProvider = TomTomMapProvider
} else if mapProvider == TianDiTuProvider {
config.MapProvider = TianDiTuProvider
} else if mapProvider == GoogleMapProvider {
config.MapProvider = GoogleMapProvider
} else if mapProvider == BaiduMapProvider {
config.MapProvider = BaiduMapProvider
} else if mapProvider == AmapProvider {
config.MapProvider = AmapProvider
} else if mapProvider == CustomProvider {
config.MapProvider = CustomProvider
} else {
return errs.ErrInvalidMapProvider
}
config.EnableMapDataFetchProxy = getConfigItemBoolValue(configFile, sectionName, "map_data_fetch_proxy", false)
config.MapProxy = getConfigItemStringValue(configFile, sectionName, "proxy", "system")
config.TomTomMapAPIKey = getConfigItemStringValue(configFile, sectionName, "tomtom_map_api_key")
config.TianDiTuAPIKey = getConfigItemStringValue(configFile, sectionName, "tianditu_map_app_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")
@@ -569,6 +812,12 @@ func loadMapConfiguration(config *Config, configFile *ini.File, sectionName stri
config.AmapApplicationSecret = getConfigItemStringValue(configFile, sectionName, "amap_application_secret")
config.AmapApiExternalProxyUrl = getConfigItemStringValue(configFile, sectionName, "amap_api_external_proxy_url")
config.CustomMapTileServerTileLayerUrl = getConfigItemStringValue(configFile, sectionName, "custom_map_tile_server_url")
config.CustomMapTileServerAnnotationLayerUrl = getConfigItemStringValue(configFile, sectionName, "custom_map_tile_server_annotation_url")
config.CustomMapTileServerMinZoomLevel = getConfigItemUint8Value(configFile, sectionName, "custom_map_tile_server_min_zoom_level", 1)
config.CustomMapTileServerMaxZoomLevel = getConfigItemUint8Value(configFile, sectionName, "custom_map_tile_server_max_zoom_level", 18)
config.CustomMapTileServerDefaultZoomLevel = getConfigItemUint8Value(configFile, sectionName, "custom_map_tile_server_default_zoom_level", 14)
return nil
}
func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectionName string) error {
@@ -590,6 +839,7 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio
return errs.ErrInvalidExchangeRatesDataSource
}
config.ExchangeRatesProxy = getConfigItemStringValue(configFile, sectionName, "proxy", "system")
config.ExchangeRatesRequestTimeout = getConfigItemUint32Value(configFile, sectionName, "request_timeout", defaultExchangeRatesDataRequestTimeout)
config.ExchangeRatesSkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "skip_tls_verify", false)
@@ -626,6 +876,44 @@ func getFinalPath(workingPath, p string) (string, error) {
return p, err
}
func getNotificationConfiguration(configFile *ini.File, sectionName string, enableKey string, contentKey string) NotificationConfig {
config := NotificationConfig{
Enabled: getConfigItemBoolValue(configFile, sectionName, enableKey, false),
DefaultContent: getConfigItemStringValue(configFile, sectionName, contentKey, ""),
MultiLanguageContent: make(map[string]string),
}
for languageTag := range locales.AllLanguages {
multiLanguageContentKey := strings.ToLower(languageTag)
multiLanguageContentKey = strings.Replace(multiLanguageContentKey, "-", "_", -1)
multiLanguageContentKey = contentKey + "_" + multiLanguageContentKey
content := getConfigItemStringValue(configFile, sectionName, multiLanguageContentKey, "")
if content != "" {
config.MultiLanguageContent[languageTag] = content
}
}
return config
}
func getConfigItemIsSet(configFile *ini.File, sectionName string, itemName string) bool {
environmentKey := getEnvironmentKey(sectionName, itemName)
environmentValue := os.Getenv(environmentKey)
if len(environmentValue) > 0 {
return true
}
section := configFile.Section(sectionName)
if !section.HasKey(itemName) {
return false
}
return section.Key(itemName).String() != ""
}
func getConfigItemStringValue(configFile *ini.File, sectionName string, itemName string, defaultValue ...string) string {
environmentKey := getEnvironmentKey(sectionName, itemName)
environmentValue := os.Getenv(environmentKey)
@@ -729,14 +1017,16 @@ func getEnvironmentKey(sectionName string, itemName string) string {
return fmt.Sprintf("%s_%s_%s", ebkEnvNamePrefix, strings.ToUpper(sectionName), strings.ToUpper(itemName))
}
func getLogLevel(logLevelStr string, defaultLogLevel Level) Level {
func getLogLevel(logLevelStr string) (Level, error) {
if logLevelStr == "debug" {
return LOGLEVEL_DEBUG
return LOGLEVEL_DEBUG, nil
} else if logLevelStr == "info" {
return LOGLEVEL_INFO, nil
} else if logLevelStr == "warn" {
return LOGLEVEL_WARN
return LOGLEVEL_WARN, nil
} else if logLevelStr == "error" {
return LOGLEVEL_ERROR
return LOGLEVEL_ERROR, nil
}
return defaultLogLevel
return "", errs.ErrInvalidLogLevel
}
+57
View File
@@ -16,3 +16,60 @@ var (
func SetCurrentConfig(config *Config) {
Container.Current = config
}
// GetAfterRegisterNotificationContent returns the notification content displayed each time users register
func (c *ConfigContainer) GetAfterRegisterNotificationContent(userLanguage string, clientLanguage string) string {
language := userLanguage
if language == "" {
language = clientLanguage
}
if !c.Current.AfterRegisterNotification.Enabled {
return ""
}
if multiLanguageContent, exists := c.Current.AfterRegisterNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return c.Current.AfterRegisterNotification.DefaultContent
}
// GetAfterLoginNotificationContent returns the notification content displayed each time users log in
func (c *ConfigContainer) GetAfterLoginNotificationContent(userLanguage string, clientLanguage string) string {
language := userLanguage
if language == "" {
language = clientLanguage
}
if !c.Current.AfterLoginNotification.Enabled {
return ""
}
if multiLanguageContent, exists := c.Current.AfterLoginNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return c.Current.AfterLoginNotification.DefaultContent
}
// GetAfterOpenNotificationContent returns the notification content displayed each time users open the app
func (c *ConfigContainer) GetAfterOpenNotificationContent(userLanguage string, clientLanguage string) string {
language := userLanguage
if language == "" {
language = clientLanguage
}
if !c.Current.AfterOpenNotification.Enabled {
return ""
}
if multiLanguageContent, exists := c.Current.AfterOpenNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return c.Current.AfterOpenNotification.DefaultContent
}
+68
View File
@@ -0,0 +1,68 @@
package storage
import (
"io"
"os"
"path/filepath"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// LocalFileSystemObjectStorage represents local file system object storage
type LocalFileSystemObjectStorage struct {
rootPath string
}
// NewLocalFileSystemObjectStorage returns a local file system object storage
func NewLocalFileSystemObjectStorage(config *settings.Config, pathPrefix string) (*LocalFileSystemObjectStorage, error) {
storage := &LocalFileSystemObjectStorage{
rootPath: filepath.Join(config.LocalFileSystemPath, pathPrefix),
}
if err := os.MkdirAll(storage.rootPath, os.ModePerm); err != nil {
return nil, err
}
return storage, nil
}
// Exists returns whether the file exists
func (s *LocalFileSystemObjectStorage) Exists(path string) (bool, error) {
return utils.IsExists(s.getFinalPath(path))
}
// Read returns the object instance according to specified the file path
func (s *LocalFileSystemObjectStorage) Read(path string) (ObjectInStorage, error) {
return os.Open(s.getFinalPath(path))
}
// Save returns whether save the object instance successfully
func (s *LocalFileSystemObjectStorage) Save(path string, object ObjectInStorage) error {
finalPath := s.getFinalPath(path)
if err := os.MkdirAll(filepath.Dir(finalPath), os.ModePerm); err != nil {
return err
}
targetFile, err := os.Create(finalPath)
if err != nil {
return err
}
defer targetFile.Close()
_, err = io.Copy(targetFile, object)
return err
}
// Delete returns whether delete the object according to specified the file path successfully
func (s *LocalFileSystemObjectStorage) Delete(path string) error {
return os.Remove(s.getFinalPath(path))
}
func (s *LocalFileSystemObjectStorage) getFinalPath(path string) string {
return filepath.Join(s.rootPath, path)
}
+108
View File
@@ -0,0 +1,108 @@
package storage
import (
"context"
"crypto/tls"
"net/http"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// MinIOObjectStorage represents MinIO object storage
type MinIOObjectStorage struct {
minIOClient *minio.Client
minIOConfig *settings.MinIOConfig
rootPath string
}
// NewMinIOObjectStorage returns a MinIO object storage
func NewMinIOObjectStorage(config *settings.Config, pathPrefix string) (*MinIOObjectStorage, error) {
minIOConfig := config.MinIOConfig
minIOClient, err := minio.New(minIOConfig.Endpoint, &minio.Options{
Region: minIOConfig.Location,
Creds: credentials.NewStaticV4(minIOConfig.AccessKeyID, minIOConfig.SecretAccessKey, ""),
Secure: minIOConfig.UseSSL,
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: minIOConfig.SkipTLSVerify}},
})
if err != nil {
return nil, err
}
storage := &MinIOObjectStorage{
minIOClient: minIOClient,
minIOConfig: minIOConfig,
rootPath: minIOConfig.RootPath,
}
storage.rootPath = storage.getFinalPath(pathPrefix)
ctx := context.Background()
exists, err := minIOClient.BucketExists(ctx, minIOConfig.Bucket)
if err != nil {
return nil, err
}
if !exists {
err := minIOClient.MakeBucket(ctx, minIOConfig.Bucket, minio.MakeBucketOptions{
Region: minIOConfig.Location,
})
if err != nil {
return nil, err
}
}
return storage, nil
}
// Exists returns whether the file exists
func (s *MinIOObjectStorage) Exists(path string) (bool, error) {
ctx := context.Background()
objectInfo, err := s.minIOClient.StatObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.StatObjectOptions{})
if err == nil && !objectInfo.IsDeleteMarker {
return true, nil
}
return false, err
}
// Read returns the object instance according to specified the file path
func (s *MinIOObjectStorage) Read(path string) (ObjectInStorage, error) {
ctx := context.Background()
return s.minIOClient.GetObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.GetObjectOptions{})
}
// Save returns whether save the object instance successfully
func (s *MinIOObjectStorage) Save(path string, object ObjectInStorage) error {
ctx := context.Background()
_, err := s.minIOClient.PutObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), object, -1, minio.PutObjectOptions{})
return err
}
// Delete returns whether delete the object according to specified the file path successfully
func (s *MinIOObjectStorage) Delete(path string) error {
ctx := context.Background()
return s.minIOClient.RemoveObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.RemoveObjectOptions{})
}
func (s *MinIOObjectStorage) getFinalPath(path string) string {
rootPath := s.rootPath
if len(rootPath) < 1 || rootPath[len(rootPath)-1] != '/' {
rootPath = rootPath + "/"
}
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
return rootPath + path
}
+11
View File
@@ -0,0 +1,11 @@
package storage
import (
"io"
)
// ObjectInStorage represents the object instance in the storage
type ObjectInStorage interface {
io.ReadCloser
io.Seeker
}
+9
View File
@@ -0,0 +1,9 @@
package storage
// ObjectStorage represents an object storage to store file object
type ObjectStorage interface {
Exists(path string) (bool, error)
Read(path string) (ObjectInStorage, error)
Save(path string, object ObjectInStorage) error
Delete(path string) error
}
+61
View File
@@ -0,0 +1,61 @@
package storage
import (
"fmt"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const avatarPathPrefix = "avatar"
// StorageContainer contains the current object storage
type StorageContainer struct {
AvatarCurrentStorage ObjectStorage
}
// Initialize a object storage container singleton instance
var (
Container = &StorageContainer{}
)
// InitializeStorageContainer initializes the current object storage according to the config
func InitializeStorageContainer(config *settings.Config) error {
if config.StorageType == settings.LocalFileSystemObjectStorageType {
avatarStorage, err := NewLocalFileSystemObjectStorage(config, avatarPathPrefix)
Container.AvatarCurrentStorage = avatarStorage
return err
} else if config.StorageType == settings.MinIOStorageType {
avatarStorage, err := NewMinIOObjectStorage(config, avatarPathPrefix)
Container.AvatarCurrentStorage = avatarStorage
return err
}
return errs.ErrInvalidStorageType
}
// ExistsAvatar returns whether the user avatar exists from the current object storage
func (s *StorageContainer) ExistsAvatar(uid int64, fileExtension string) (bool, error) {
return s.AvatarCurrentStorage.Exists(s.getUserAvatarPath(uid, fileExtension))
}
// ReadAvatar returns the user avatar from the current object storage
func (s *StorageContainer) ReadAvatar(uid int64, fileExtension string) (ObjectInStorage, error) {
return s.AvatarCurrentStorage.Read(s.getUserAvatarPath(uid, fileExtension))
}
// SaveAvatar returns whether save the user avatar into the current object storage successfully
func (s *StorageContainer) SaveAvatar(uid int64, object ObjectInStorage, fileExtension string) error {
return s.AvatarCurrentStorage.Save(s.getUserAvatarPath(uid, fileExtension), object)
}
// DeleteAvatar returns whether delete the user avatar from the current object storage successfully
func (s *StorageContainer) DeleteAvatar(uid int64, fileExtension string) error {
return s.AvatarCurrentStorage.Delete(s.getUserAvatarPath(uid, fileExtension))
}
func (s *StorageContainer) getUserAvatarPath(uid int64, fileExtension string) string {
return fmt.Sprintf("%d.%s", uid, fileExtension)
}
+2
View File
@@ -110,6 +110,8 @@ func getValidationErrorText(err validator.FieldError) string {
return errs.GetParameterInvalidCurrencyMessage(fieldName)
case "validHexRGBColor":
return errs.GetParameterInvalidHexRGBColorMessage(fieldName)
case "validAmountFilter":
return errs.GetParameterInvalidAmountFilterMessage(fieldName)
}
return errs.GetParameterInvalidMessage(fieldName)

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