Compare commits

..

872 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
MaysWind 55d06640b5 auto hide forget password sheet after email has been resent 2023-09-17 19:37:26 +08:00
MaysWind 3874a8da21 clear email when open forget password sheet 2023-09-17 19:36:36 +08:00
MaysWind 6a3ecd5b09 add cancel button to forget password sheet 2023-09-17 19:34:20 +08:00
MaysWind afcd4f7262 support resending verify mail on mobile device 2023-09-17 19:32:12 +08:00
MaysWind b0ae731fa6 update golang to 1.20.8 2023-09-17 18:12:55 +08:00
MaysWind 0b678fe69a code refactor 2023-09-17 18:07:09 +08:00
MaysWind 4cecc78a74 don't show verify email has sent to when there are no valid verify tokens 2023-09-17 18:01:10 +08:00
MaysWind 92273d2fc6 add skip_tls_verify option for exchange rates 2023-09-17 17:24:41 +08:00
MaysWind 04ec749c3c fix the bug that old uuid may be generated sometimes 2023-09-16 23:53:45 +08:00
MaysWind 165377816c return error when uuid is not enough 2023-09-16 22:45:00 +08:00
MaysWind 729904e1c3 remove unused code 2023-09-15 21:54:25 +08:00
MaysWind 6bc4fa0a82 update vuetify to 3.3.16 2023-09-15 21:41:28 +08:00
MaysWind f12403672b update README 2023-09-10 23:30:44 +08:00
MaysWind 97daff8d4f update mobile screenshot img url 2023-09-10 22:12:36 +08:00
MaysWind a32451fd7f hide resend verify email button when server disables verify email 2023-09-10 17:29:10 +08:00
MaysWind ca14770971 return error entity when verify email is not enabled 2023-09-10 17:25:23 +08:00
MaysWind 35ac0695c7 don't show send mail tips when force verify email is enabled but verify email is not enabled 2023-09-10 17:15:56 +08:00
MaysWind 589b614a53 remove old email verify token before send new verify email when email changed 2023-09-10 17:00:19 +08:00
MaysWind 64ea3e05d8 aysnc send email 2023-09-10 16:49:24 +08:00
MaysWind 5d0e115438 auto send verify email when user email has been changed 2023-09-10 16:40:48 +08:00
MaysWind 9f35c1eded improve user registration page 2023-09-10 16:21:29 +08:00
MaysWind ff07346fe9 modify style 2023-09-10 00:25:52 +08:00
MaysWind 22fffc2f8c profile page supports resending verify email 2023-09-10 00:25:42 +08:00
MaysWind 205363dd42 change link text when email is verified 2023-09-09 21:33:17 +08:00
MaysWind d2297b882f send verify email after account has been registered 2023-09-09 21:28:17 +08:00
MaysWind 48bf8dbc5b don't create temporary token when smtp is not enabled 2023-09-09 21:25:30 +08:00
MaysWind 9585c760d5 modify style 2023-09-09 16:39:28 +08:00
MaysWind 8c2cd0aa4d modify style 2023-09-09 15:56:21 +08:00
MaysWind 2e680b04c9 supports building for different platforms for gitea actions 2023-09-04 23:33:52 +08:00
MaysWind e2b81f7b57 add email verification 2023-09-04 23:30:33 +08:00
MaysWind c38b277887 disabled user cannot use forget password 2023-09-03 23:35:01 +08:00
MaysWind a1f6304b22 fix log content is wrong when content has % symbol 2023-09-03 23:15:40 +08:00
MaysWind 09d7f56efc modify log formatter 2023-09-03 13:56:19 +08:00
MaysWind 9221f3fc96 add request id to sql query log 2023-09-03 13:54:07 +08:00
MaysWind 6b30a0aebc supports building multiple image by gitea actions 2023-09-02 19:33:48 +08:00
MaysWind 158563f387 code refactor 2023-09-02 00:05:31 +08:00
MaysWind b0fc5752e2 upgrade vuetify to 3.3.15 2023-09-01 23:28:18 +08:00
MaysWind 28903615ed fix the problem that unit test would not run sometimes 2023-08-28 23:06:03 +08:00
MaysWind caa83c0432 modify text 2023-08-28 00:16:16 +08:00
MaysWind 045f4a42db replace the app name in the email with the one configured in the config file 2023-08-28 00:11:42 +08:00
MaysWind 3275bc9cae redirect page to login page after reset password successfully 2023-08-27 23:58:00 +08:00
MaysWind 6d14eaefe1 code refactor 2023-08-27 23:51:26 +08:00
MaysWind 03274725be modify text and field name 2023-08-27 23:28:38 +08:00
MaysWind 0951006063 add updating user email verified state utility 2023-08-27 22:54:34 +08:00
MaysWind 616bfc6a2a not allow send password reset mail when email address is not verified 2023-08-27 22:35:16 +08:00
MaysWind c0bfe429ee the language of password reset email set to client language if user language is not set 2023-08-27 22:29:54 +08:00
MaysWind c1d90485a1 reset password don't set authorization header 2023-08-27 22:21:10 +08:00
MaysWind 6c30527684 fix typo 2023-08-27 21:37:22 +08:00
MaysWind 4ac751f492 add send user password reset email command line utility 2023-08-27 21:37:10 +08:00
MaysWind de7b137257 add send test email utility 2023-08-27 21:26:30 +08:00
MaysWind 0bf689fa8d code refactor 2023-08-27 21:23:03 +08:00
MaysWind f31ef1649f support reset password by email reset link 2023-08-27 21:22:52 +08:00
MaysWind c66bc62c41 code refactor 2023-08-26 23:35:53 +08:00
MaysWind 9ee59215c8 code refactor 2023-08-26 23:14:42 +08:00
MaysWind a991adecaf modify style 2023-08-26 19:36:39 +08:00
MaysWind 601b1d5c89 modify style 2023-08-26 19:10:06 +08:00
MaysWind 8b593883a7 modify style 2023-08-26 18:22:19 +08:00
MaysWind bb549c9a89 modify text 2023-08-26 17:57:42 +08:00
MaysWind 9f2005622a fix color is wrong in dark theme 2023-08-26 17:11:29 +08:00
MaysWind 4238bde13a modify style 2023-08-26 00:21:00 +08:00
MaysWind 2d4ce1aac0 modify style 2023-08-26 00:12:29 +08:00
MaysWind 37269abde2 update go.sum 2023-08-26 00:02:05 +08:00
MaysWind 54038eabd4 upgrade vuetify to 3.3.14 2023-08-26 00:01:12 +08:00
MaysWind 57922e3071 modify style 2023-08-25 23:59:05 +08:00
MaysWind ad36dfd448 auto choose the secondary category of selected primary category when create transaction in transaction list page 2023-08-22 00:57:02 +08:00
MaysWind 6e334d2efb hide the category dropdown menu when click the item 2023-08-22 00:05:36 +08:00
MaysWind d3dc1401fd auto hide the dropdown menu when click the menu item 2023-08-21 23:59:48 +08:00
MaysWind 0f59c9911d modify style 2023-08-21 23:54:36 +08:00
MaysWind 0ebfd2bc76 modify style 2023-08-21 23:51:24 +08:00
MaysWind 734625c1e3 modify style 2023-08-21 23:46:34 +08:00
MaysWind eaaea8a64f modify props and fields name 2023-08-21 23:42:07 +08:00
MaysWind c026ab1777 modify style 2023-08-21 23:41:10 +08:00
MaysWind 2c7193efea don't allow show the transaction detail when the transaction type is modify balance 2023-08-21 23:35:59 +08:00
MaysWind 21f5ef469b always display account name even if the account is hidden 2023-08-21 23:35:21 +08:00
MaysWind e7f9eb6e06 modify style 2023-08-21 23:25:15 +08:00
MaysWind 9db9c382ab modify text 2023-08-21 23:16:12 +08:00
MaysWind b2ba626cde hide the buttons which is not supported 2023-08-21 23:15:45 +08:00
MaysWind 85d93c4f4b auto hide the dropdown menu when click the secondary item 2023-08-21 23:02:46 +08:00
MaysWind 09f6dd8d82 add more log in unit test 2023-08-21 01:41:03 +08:00
MaysWind 2aa6df48c6 remove unused code 2023-08-21 01:16:43 +08:00
MaysWind 2a16260b05 transaction edit dialog supports duplicate transaction, edit transaction and save new transaction 2023-08-21 01:11:01 +08:00
MaysWind c28158b041 remove unused code 2023-08-21 00:00:25 +08:00
MaysWind 661199850c remove unused code 2023-08-20 23:53:41 +08:00
MaysWind 67b69a45cc modify style 2023-08-20 23:33:14 +08:00
MaysWind 579322578b modify style 2023-08-20 22:21:50 +08:00
MaysWind 0de65042b0 transaction edit dialog supports transaction category 2023-08-20 20:31:29 +08:00
MaysWind f9643f8651 modify method name 2023-08-20 19:43:22 +08:00
MaysWind fc9ac4c40e modify method name 2023-08-20 19:41:51 +08:00
MaysWind 84843066f2 fix style 2023-08-20 19:23:13 +08:00
MaysWind af3d5c4654 remove unused code 2023-08-20 17:35:11 +08:00
MaysWind 676f3daf50 modify style 2023-08-20 17:30:47 +08:00
MaysWind 54970015cb update third party dependencies 2023-08-20 17:17:41 +08:00
MaysWind 2b49430530 update golang to 1.20.7, update node.js to 18.17.1, update alpine base image to 3.17.5 2023-08-20 17:01:01 +08:00
MaysWind feea5f3518 modify style 2023-08-20 16:57:00 +08:00
MaysWind 04e580b40f modify style 2023-08-20 01:22:00 +08:00
MaysWind f07a3ba97d code refactor 2023-08-20 01:09:23 +08:00
MaysWind f44e3a81ab modify style 2023-08-20 01:08:26 +08:00
MaysWind 77d3bd019e fix the problem that the thousands separator is missing 2023-08-20 00:57:01 +08:00
MaysWind 72725d5ab4 hide set as base currency when the currency is already the base 2023-08-20 00:54:11 +08:00
MaysWind aa717ed1fe fix the problem that the time zone of the modify balance transaction generated by creating a new account was wrong 2023-08-20 00:48:42 +08:00
MaysWind 292b49ba79 modify style 2023-08-20 00:40:56 +08:00
MaysWind c600eb5d5a modify style 2023-08-20 00:33:57 +08:00
MaysWind 6d2f788fa2 code refactor 2023-08-20 00:30:55 +08:00
MaysWind b8acff3e7c code refactor 2023-08-20 00:30:30 +08:00
MaysWind b8bdb03fc0 transaction edit dialog supports transaction time 2023-08-19 22:41:13 +08:00
MaysWind 7257abefb4 fix AM/PM text in datetime picker is not translated 2023-08-19 22:37:26 +08:00
MaysWind 3095613a76 transaction edit dialog supports transaction tags 2023-08-18 00:44:05 +08:00
MaysWind db12b64b3a modify style 2023-08-18 00:03:49 +08:00
MaysWind a081edde25 modify style 2023-08-17 23:01:52 +08:00
MaysWind 5496c4a10a code refactor 2023-08-16 00:51:12 +08:00
MaysWind c968ded99a transaction edit dialog supports map 2023-08-16 00:51:03 +08:00
MaysWind 4224a833b4 code refactor 2023-08-16 00:04:50 +08:00
MaysWind df470f1a5e modify style 2023-08-15 22:53:09 +08:00
MaysWind ed0100a82c add more transaction edit dialog basis code 2023-08-15 01:05:59 +08:00
MaysWind 0ad72e8334 modify style 2023-08-14 23:13:03 +08:00
MaysWind 50ee5d1f49 hide control buttons when loading 2023-08-14 23:12:35 +08:00
MaysWind 94283a8da2 update unit test 2023-08-14 22:47:54 +08:00
MaysWind 86e9a3e838 add transaction edit dialog basis code 2023-08-14 01:03:43 +08:00
MaysWind d77b9ef7c9 code refactor 2023-08-14 00:55:41 +08:00
MaysWind e29afa3155 code refactor 2023-08-14 00:14:53 +08:00
MaysWind 7376fbe7a1 code refactor 2023-08-13 23:58:28 +08:00
MaysWind 04e98e1c39 code refactor 2023-08-13 23:40:29 +08:00
MaysWind ddca6e7ec9 code refactor 2023-08-13 23:33:07 +08:00
MaysWind 1ac12ac668 remove unused code 2023-08-13 23:03:30 +08:00
MaysWind 399936e3ab code refactor 2023-08-13 22:58:52 +08:00
MaysWind 3d086992dc modify style 2023-08-13 22:52:31 +08:00
MaysWind 62ded1290c code refactor 2023-08-13 22:19:59 +08:00
MaysWind 33cbdfbd13 code refactor 2023-08-13 20:48:32 +08:00
MaysWind f5ce6ed4bc code refactor 2023-08-13 20:32:51 +08:00
MaysWind eb9a2ac2e0 code refactor 2023-08-13 20:13:27 +08:00
MaysWind 8bed529d05 code refactor 2023-08-13 20:06:42 +08:00
MaysWind 41a8b8007a code refactor 2023-08-13 20:01:44 +08:00
MaysWind 141dc843f3 modify method name 2023-08-13 19:52:58 +08:00
MaysWind 902825404e support disable boot log 2023-08-13 18:28:08 +08:00
MaysWind 749eaaab30 add parsing request id command utility 2023-08-13 18:19:09 +08:00
MaysWind 8f5767b992 add unit test 2023-08-13 17:27:25 +08:00
MaysWind 715f0c5853 add unittest 2023-08-13 17:08:36 +08:00
MaysWind a960fd3d56 improve unit test 2023-08-13 16:52:20 +08:00
MaysWind 17b8ac6d0b fix gravatar url is invalid when email contains upper characters 2023-08-13 15:37:48 +08:00
MaysWind 4ac78fe4d1 modify method name 2023-08-13 15:30:20 +08:00
MaysWind 957bcf790f code refactor 2023-08-13 15:18:38 +08:00
MaysWind 66f0b38008 code refactor 2023-08-13 15:03:31 +08:00
MaysWind f6a2246aab code refactor 2023-08-13 14:58:11 +08:00
MaysWind fa3e941069 show login link in sign up page 2023-08-13 14:46:06 +08:00
MaysWind 9bb049f746 modify style 2023-08-13 14:21:53 +08:00
MaysWind c41f2a4d65 modify style 2023-08-13 14:20:40 +08:00
MaysWind 06ff9f2499 add add/edit account dialog 2023-08-13 02:00:14 +08:00
MaysWind f91f9fcc94 modify style 2023-08-13 01:35:11 +08:00
MaysWind 9a626b0d4f modify dialog style 2023-08-13 01:34:40 +08:00
MaysWind ac2adaf4ba modify style 2023-08-12 18:16:53 +08:00
MaysWind 3ab198615b code refactor 2023-08-12 13:05:03 +08:00
MaysWind 7c2831098c clear displayed transactions when changing filter 2023-08-12 00:28:32 +08:00
MaysWind 2454e22ea2 support setting items per page in transaction list page 2023-08-11 23:58:30 +08:00
MaysWind b1fb40ca61 modify style 2023-08-08 01:39:50 +08:00
MaysWind e403e938c3 show category comment in list 2023-08-08 01:17:59 +08:00
MaysWind 952ba1f1ea code refactor 2023-08-07 01:32:11 +08:00
MaysWind a25690c2d3 modify style 2023-08-07 01:31:34 +08:00
MaysWind 6b6b9c61d7 auto complete supports auto selected the first item by enter / tab key 2023-08-07 01:14:47 +08:00
MaysWind fcc5e522a7 modify style 2023-08-07 01:10:07 +08:00
MaysWind c33c0487cf add add/edit transaction category dialog 2023-08-07 01:00:02 +08:00
MaysWind 1753a6c247 manually set default icon color 2023-08-07 00:48:48 +08:00
MaysWind 195f513416 code refactor 2023-08-07 00:38:08 +08:00
MaysWind c6d38bb3d7 code refactor 2023-08-06 23:57:23 +08:00
MaysWind 9b2fba9600 fix the problem that not scroll to selected item in color selection sheet or icon icon selection sheet 2023-08-06 23:35:51 +08:00
MaysWind c88f6501fa don't auto hide sheet when select icon 2023-08-06 23:30:10 +08:00
MaysWind c511346160 add missing component 2023-08-06 20:42:36 +08:00
MaysWind 0b02daac1d modify file name 2023-08-06 18:58:05 +08:00
MaysWind 2390c085e4 modify style 2023-08-06 18:52:55 +08:00
MaysWind 698d94feed code refactor 2023-08-05 23:04:29 +08:00
MaysWind a9a6d39127 update vuetify to 3.3.11 2023-08-05 17:14:59 +08:00
MaysWind 395bd31898 move files 2023-08-05 16:51:34 +08:00
MaysWind 7e24492ce8 add category preset for desktop page 2023-08-04 01:00:19 +08:00
MaysWind 19d3d80013 add category preset for desktop page 2023-08-04 00:56:26 +08:00
MaysWind 8c7875d7ea modify text 2023-08-03 18:21:19 +08:00
MaysWind 110ce0d4c6 modify skeleton style 2023-08-02 00:53:12 +08:00
MaysWind ff8c57fdab add transaction category list page 2023-08-02 00:49:37 +08:00
MaysWind 54ccdc57bf modify field name 2023-08-02 00:24:59 +08:00
MaysWind 2463f06ba1 only show add default categories button when really have no category 2023-08-02 00:14:09 +08:00
MaysWind 88d5b1f98f set drag handle disabled when loading or updating 2023-08-02 00:01:12 +08:00
MaysWind 43522e9c81 add all date type range in transaction list page 2023-08-01 22:33:12 +08:00
MaysWind 6e798f739f support click in trend chart in overview page 2023-08-01 22:15:15 +08:00
MaysWind 5b5f1280af fix the problem that the transaction date not display sometimes in transaction list page 2023-08-01 21:47:26 +08:00
MaysWind 2188e8dd78 fix wrong timeout 2023-08-01 09:20:02 +08:00
MaysWind 8659e9ea37 add asset summary card in home page, add 6 more months in trend card 2023-08-01 09:19:33 +08:00
MaysWind 4cff481d61 fix the problem that the monthly total income/expense amount sometimes is wrong 2023-07-31 10:05:30 +08:00
MaysWind 6da910d8fb modify style 2023-07-31 01:22:35 +08:00
MaysWind cc08ad46e3 modify overview page loading style 2023-07-31 01:21:47 +08:00
MaysWind 5b52c1adbb show no data in trend card when there are really no data in recent 6 months 2023-07-30 23:57:47 +08:00
MaysWind a20958a89b adjust display order of expense and income 2023-07-30 23:30:05 +08:00
MaysWind dea36d4b80 add trend in income and expense card in overview page 2023-07-30 23:26:10 +08:00
MaysWind 6cb7e4caf7 modify style 2023-07-30 22:59:03 +08:00
MaysWind 6e41668b25 add more runtime caching pattern 2023-07-30 00:43:04 +08:00
MaysWind d3e1acddc5 code refactor, modify style 2023-07-30 00:26:32 +08:00
MaysWind a9c511eb2e fix the problem that cannot search the keywords which contains & symbol in the transaction list page 2023-07-29 23:02:34 +08:00
MaysWind ffef33a9fc code refactor, set category menu disabled when type is modify balance, modify style 2023-07-29 17:26:50 +08:00
MaysWind 982917ddbb set export data button disabled if no data can be exported 2023-07-29 15:39:50 +08:00
MaysWind 07406a50bb show account icon in user basic setting tab, always show default account name even if the account is hidden, set the submit button disabled when nothing has been changed 2023-07-29 15:35:55 +08:00
MaysWind 78f325e127 code refactor 2023-07-29 15:13:37 +08:00
MaysWind e1bb97a1db improve runtime caching pattern 2023-07-27 00:01:17 +08:00
MaysWind 831952806d hide hidden category in transaction list page 2023-07-26 23:37:43 +08:00
MaysWind 4c57b7a009 fix the border not show in some device 2023-07-26 23:33:01 +08:00
MaysWind 683188f67a code refactor 2023-07-24 23:36:04 +08:00
MaysWind 848e5271ab code refactor 2023-07-24 02:41:00 +08:00
MaysWind 7ca5614c44 remove unused code 2023-07-24 02:40:20 +08:00
MaysWind 70fc781a03 add transaction list page for desktop 2023-07-24 02:36:59 +08:00
MaysWind aafdbab781 fix the problem that the time zone setting did not take effect immediately 2023-07-24 02:23:19 +08:00
MaysWind 5dd0f7ea10 only add date time params to transaction list url when date range type is set to custom in transaction statistics page 2023-07-24 00:22:30 +08:00
MaysWind 9393d9105c add persistent props to date range selection dialog 2023-07-24 00:20:43 +08:00
MaysWind 2c3856be3c change url when switching tab in user/app settings page 2023-07-24 00:20:01 +08:00
MaysWind dc746a51a5 add set as baseline button to exchange rates page for desktop 2023-07-24 00:16:43 +08:00
MaysWind 0f2c9354f0 modify style and hide operation buttons when cursor not hovered on 2023-07-24 00:16:09 +08:00
MaysWind af00032ee9 check whether user is logined when entering every page 2023-07-23 12:00:18 +08:00
MaysWind 5d7e685dc4 use icon to replace symbol character 2023-07-23 01:32:55 +08:00
MaysWind bfcb79c02b modify style 2023-07-22 23:49:55 +08:00
MaysWind ebee6273b0 code refactor 2023-07-22 23:17:13 +08:00
MaysWind b45900e5bc modify text 2023-07-22 23:06:00 +08:00
MaysWind 9f0657683a modify style 2023-07-22 23:04:08 +08:00
MaysWind 35b8d8ca25 modify style 2023-07-21 00:47:13 +08:00
MaysWind 8f7095ce19 transaction tag list page supports dragging to change display order 2023-07-20 23:52:33 +08:00
MaysWind d9c8dd20e9 modify style 2023-07-20 23:29:26 +08:00
MaysWind 8dcc462a30 modify style 2023-07-20 23:16:00 +08:00
MaysWind b561948030 code refactor 2023-07-20 01:37:45 +08:00
MaysWind 2cbd8684cf account list page supports dragging to change display order 2023-07-19 02:32:10 +08:00
MaysWind 107c9fce94 fix default icon color 2023-07-19 00:24:00 +08:00
MaysWind 4c18b3e059 optimize vite build config 2023-07-18 23:52:49 +08:00
MaysWind 9960ec4d58 modify style 2023-07-18 23:15:39 +08:00
MaysWind 2a09e048e4 code refactor 2023-07-18 00:51:47 +08:00
MaysWind 9711f3ba72 modify style 2023-07-18 00:48:54 +08:00
MaysWind 0622d2b81b modify style 2023-07-18 00:46:37 +08:00
MaysWind a372d1fb60 code refactor 2023-07-18 00:41:26 +08:00
MaysWind 99e55e730e code refactor 2023-07-18 00:29:41 +08:00
MaysWind 57d1e915e6 remove unused function reference 2023-07-18 00:18:02 +08:00
MaysWind 96ad6228bd code refactor 2023-07-18 00:17:38 +08:00
MaysWind 678f9fbe87 fix wrong text 2023-07-17 23:14:35 +08:00
MaysWind 714933df56 code refactor 2023-07-17 23:13:54 +08:00
MaysWind f0ef9ad51e remove unused code 2023-07-17 23:05:48 +08:00
MaysWind 0255213934 code refactor 2023-07-17 23:02:51 +08:00
MaysWind 0ad92a999c modify style 2023-07-17 00:35:27 +08:00
MaysWind b06456d573 modify style 2023-07-17 00:13:57 +08:00
MaysWind 6f1fc2c9b4 modify field name 2023-07-17 00:00:12 +08:00
MaysWind 44a1982d87 code refactor 2023-07-16 23:57:18 +08:00
MaysWind 6b0cf5aa96 fix missing plus symbol issue when there are unsupported currencies of sub accounts 2023-07-16 23:56:56 +08:00
MaysWind 2cfac7a772 modify style 2023-07-16 23:48:04 +08:00
MaysWind 99aaf35e0b add account list page for desktop 2023-07-16 23:35:50 +08:00
MaysWind 942ed1fb55 code refactor 2023-07-16 23:12:44 +08:00
MaysWind 25f83a98e3 code refactor 2023-07-16 23:05:11 +08:00
MaysWind ed4040f2ec code refactor 2023-07-16 22:55:02 +08:00
MaysWind 41034de676 modify style 2023-07-16 18:35:30 +08:00
MaysWind 2db0f1a6c8 code refactor 2023-07-16 16:36:47 +08:00
MaysWind 6b06cc7ef5 desktop pie chart supports clicking the chart to scroll to the specified legend 2023-07-16 16:23:56 +08:00
MaysWind 2bf26212af modify transaction statistics page style 2023-07-16 15:28:43 +08:00
MaysWind 9d273c172d modify exchange rates page style 2023-07-16 15:19:44 +08:00
MaysWind 6ae3bc82bb modify style 2023-07-16 14:03:14 +08:00
MaysWind c782002274 modify style 2023-07-16 13:16:43 +08:00
MaysWind cd0906d041 update third party dependencies 2023-07-15 23:45:43 +08:00
MaysWind f936ecca33 manually get backend dependencies before lint checking 2023-07-15 23:44:48 +08:00
MaysWind 8794e3bc53 set confirm and more button disabled when there are no available items 2023-07-15 23:43:44 +08:00
MaysWind 4503e2a222 add necessary vuetify component and remove unnecessary vuetify component 2023-07-15 23:43:06 +08:00
MaysWind 015725f88c support setting account/transaction category filter for statistics page 2023-07-15 16:54:56 +08:00
MaysWind 39451f0e37 code refactor 2023-07-15 16:21:30 +08:00
MaysWind 2bb0b7faa5 modify text color 2023-07-15 16:07:41 +08:00
MaysWind 8db7c3769a remove blank line 2023-07-15 16:07:25 +08:00
MaysWind d4e32de882 item icon supports custom class 2023-07-15 01:38:34 +08:00
MaysWind db75dea9ee fix typo 2023-07-15 00:50:29 +08:00
MaysWind 489bba9c4b modify class name 2023-07-15 00:30:42 +08:00
MaysWind 4accc49d60 reload sessions after password changed 2023-07-15 00:24:47 +08:00
MaysWind 5a47cd5216 modify style 2023-07-14 23:50:59 +08:00
MaysWind 439608cf27 remove unused configuration 2023-07-14 22:46:00 +08:00
MaysWind f9df7a1b5a modify style 2023-07-11 01:21:45 +08:00
MaysWind 78c801994a fix issue after packing js file 2023-07-11 01:16:45 +08:00
MaysWind 882fd68cf9 add some transaction statistics settings 2023-07-11 01:01:11 +08:00
MaysWind 3433b73edf code refactor 2023-07-11 01:00:27 +08:00
MaysWind a235d6a8cd code refactor 2023-07-11 00:35:21 +08:00
MaysWind d76e88af21 code refactor 2023-07-11 00:30:57 +08:00
MaysWind 3f6c6c443a code refactor 2023-07-11 00:26:27 +08:00
MaysWind 13c6d406cf only navigate to transaction list page when click pie chart label in desktop version 2023-07-11 00:15:47 +08:00
MaysWind 19fea4e761 code refactor 2023-07-10 23:26:16 +08:00
MaysWind c84c96dcd1 update service worker config 2023-07-10 22:58:46 +08:00
MaysWind cdd8ccc2d4 modify image path 2023-07-10 22:46:52 +08:00
MaysWind 09210d5d40 remove switch to mobile/desktop version in unlock page 2023-07-10 00:38:22 +08:00
MaysWind 8f44a26037 code refactor 2023-07-10 00:14:33 +08:00
MaysWind 5e986b2d04 add transaction statistics page 2023-07-10 00:04:20 +08:00
MaysWind 298c0922cb fix account icon issue in transaction statistics data page 2023-07-09 21:21:19 +08:00
MaysWind dc127ea6a3 code refactor 2023-07-09 20:43:08 +08:00
MaysWind 522ed94c32 user settings and app settings page supports showing specified tab by query parameter 2023-07-09 16:35:12 +08:00
MaysWind 6edf66a599 code refactor 2023-07-09 14:22:47 +08:00
MaysWind 475ec24528 desktop page supports service worker 2023-07-09 13:20:24 +08:00
MaysWind b555af0df7 add lock application menu 2023-07-09 13:13:35 +08:00
MaysWind f90430e544 modify style 2023-07-09 12:45:40 +08:00
MaysWind 4ccb75818c add switch to mobile/desktop device in login/unlock page 2023-07-09 12:01:29 +08:00
MaysWind c5c9ed24c3 code refactor 2023-07-09 11:12:39 +08:00
MaysWind 386aa0adc1 code refactor 2023-07-09 01:07:10 +08:00
MaysWind 0b26b75699 modify style 2023-07-09 01:04:24 +08:00
MaysWind d013f67c70 itemicon supports hidden status 2023-07-09 00:43:42 +08:00
MaysWind dc7c0e61fd code refactor 2023-07-09 00:37:51 +08:00
MaysWind 89bd041d29 add transaction tag list page 2023-07-09 00:37:19 +08:00
MaysWind ac730d6086 remove number input stepper 2023-07-08 22:56:16 +08:00
MaysWind 427eaed544 code refactor 2023-07-08 20:31:41 +08:00
MaysWind 00f783c0b6 modify style 2023-07-08 20:28:15 +08:00
MaysWind 8e1e53d55e code refactor 2023-07-08 20:07:09 +08:00
MaysWind 5c9a5c13b8 modify style 2023-07-08 19:42:33 +08:00
MaysWind c1f03a8e75 modify style 2023-07-08 17:51:15 +08:00
MaysWind 1affa83620 code refactor 2023-07-08 16:42:35 +08:00
MaysWind 2aa8180113 code refactor 2023-07-08 16:16:28 +08:00
MaysWind e6001d83a4 modify style 2023-07-08 16:16:04 +08:00
MaysWind 8550c8fde9 modify style 2023-07-08 12:25:18 +08:00
MaysWind 48d9a09307 add sign up page 2023-07-08 02:59:50 +08:00
MaysWind 9d0b874488 code refactor 2023-07-07 23:48:32 +08:00
MaysWind 062a13b2c2 modify style 2023-07-07 22:53:22 +08:00
MaysWind 937f8723ed add background img for dark theme 2023-07-06 01:07:57 +08:00
MaysWind 0c5cabbd79 modify color 2023-07-06 00:56:10 +08:00
MaysWind 95d8f710d8 exclude unnecessary file from precaching files, add some files to runtime caching 2023-07-06 00:38:28 +08:00
MaysWind bb9b8b34e5 modify img path 2023-07-06 00:04:54 +08:00
MaysWind 54c1164bd7 add illustrations to login/unlock page 2023-07-05 01:27:05 +08:00
MaysWind 89c1158d95 modify style 2023-07-04 23:06:52 +08:00
MaysWind 9cae189941 modify text tip 2023-07-03 23:04:44 +08:00
MaysWind 53a31cd4c4 use variables to replace secrets 2023-07-03 23:02:17 +08:00
MaysWind d72a615481 code refactor 2023-07-03 23:01:31 +08:00
MaysWind 8dcffa80a8 exclude vendor files for desktop page 2023-07-02 23:50:57 +08:00
MaysWind 7cf53acd18 auto focus pin code input when open desktop unlock page 2023-07-02 23:38:15 +08:00
MaysWind 748cf68055 show avatar placeholder when loading user avatar 2023-07-02 23:34:09 +08:00
MaysWind 9cd22bdc06 add application lock setting tab for desktop 2023-07-02 23:22:59 +08:00
MaysWind 5830d4b91c desktop page support icon/startup image 2023-07-02 23:22:15 +08:00
MaysWind b42f7f566b add unlock page for desktop 2023-07-02 23:22:09 +08:00
MaysWind db8223ca98 fix the language not set to system language 2023-07-02 21:44:56 +08:00
MaysWind 9403afc392 code refactor 2023-07-02 21:44:37 +08:00
MaysWind 2b2d3b9517 code refactor 2023-07-02 20:01:59 +08:00
MaysWind 3eebdfcdb3 modify style 2023-07-02 19:31:47 +08:00
MaysWind f7766bc3d4 update Chinese translation 2023-07-02 19:28:53 +08:00
MaysWind f2614abbdd remove unused code 2023-07-02 19:25:30 +08:00
MaysWind 58824ea879 modify style 2023-07-02 19:09:40 +08:00
MaysWind 9ef7a18847 modify login page style 2023-07-02 19:07:55 +08:00
MaysWind 58c0167696 fix issue 2023-07-02 18:36:11 +08:00
MaysWind 9adfd286f9 code refactor 2023-07-02 00:51:26 +08:00
MaysWind 4e8f530fbb code refactor 2023-07-01 23:38:17 +08:00
MaysWind 3e694b0772 update Chinese translation 2023-07-01 23:27:09 +08:00
MaysWind 2fd5b04d4d code refactor 2023-07-01 23:26:31 +08:00
MaysWind 4688b9a9c9 move "Switch to Mobile Version" to "Use on Mobile Device" dialog 2023-07-01 23:16:53 +08:00
MaysWind 153e0035ba reset state after leaving 2fa setting tab 2023-07-01 23:10:49 +08:00
MaysWind 88b6ecc557 add alert component 2023-07-01 22:51:54 +08:00
MaysWind 9df55874a6 show username and avatar in user basic setting tab 2023-07-01 22:51:42 +08:00
MaysWind 87d4ab827a add reset button in user basic setting tab 2023-07-01 22:12:06 +08:00
MaysWind 318166f23a code refactor 2023-07-01 02:26:55 +08:00
MaysWind cc3f712659 remove unused code 2023-07-01 02:22:30 +08:00
MaysWind ee399d8a08 code refactor 2023-07-01 02:19:04 +08:00
MaysWind 96c233d5c5 code refactor 2023-07-01 02:05:36 +08:00
MaysWind 652b3e1954 support set gravatar as user avatar provider 2023-07-01 00:53:59 +08:00
MaysWind a8b76d5537 modify style 2023-07-01 00:15:17 +08:00
MaysWind e041b70cdd modify style 2023-07-01 00:09:15 +08:00
MaysWind 63bf912b3e upgrade vuetify to 3.3.6 2023-06-30 23:43:13 +08:00
MaysWind 0cbe7b1655 code refactor 2023-06-30 23:41:40 +08:00
MaysWind 7bbec29c5b show whether data is updated after click refresh button 2023-06-28 21:44:01 +08:00
MaysWind 7cec7dbac8 sort result in overview response 2023-06-28 21:41:59 +08:00
MaysWind 09a19b5f42 navigate to desktop page when use tablet device 2023-06-28 21:38:21 +08:00
MaysWind 8fe765c097 modify file name 2023-06-25 00:39:00 +08:00
MaysWind b11e8e07c4 set button disabled when required input is not filled 2023-06-25 00:30:48 +08:00
MaysWind f72763306d code refactor 2023-06-25 00:25:43 +08:00
MaysWind e3d1a476e2 fix wrong watching parameter 2023-06-24 23:59:53 +08:00
MaysWind f0bc86d42f add overview page 2023-06-24 23:55:52 +08:00
MaysWind a62e806175 fix wrong component reference 2023-06-24 21:51:39 +08:00
MaysWind 5bcf5bf93e show mobile url qrcode on desktop page 2023-06-24 21:28:28 +08:00
MaysWind 4f35ba0931 add settings page 2023-06-24 21:27:27 +08:00
MaysWind 2bcdfe778a code refactor 2023-06-24 21:27:19 +08:00
MaysWind c89da1d0f7 code refactor 2023-06-24 21:14:54 +08:00
MaysWind 46a1eda029 add page settings page 2023-06-24 20:01:47 +08:00
MaysWind 1c39819112 add switch to desktop/mobile link 2023-06-24 19:30:28 +08:00
MaysWind 0efe617c03 fix the problem that system default timezone is not browser timezone when custom timezone is set 2023-06-24 18:39:55 +08:00
MaysWind 10df947efe fix npe 2023-06-24 18:30:49 +08:00
MaysWind fb7790ba4a code refactor 2023-06-24 18:27:54 +08:00
MaysWind a9338ed822 code refactor 2023-06-24 14:15:15 +08:00
MaysWind eaab8cdd93 code refactor 2023-06-24 14:07:30 +08:00
MaysWind edfcd0dc6e use autocomplete to replace select 2023-06-24 01:22:35 +08:00
MaysWind 838b56089b remove blank line 2023-06-24 01:22:12 +08:00
MaysWind 178810f908 add user settings page 2023-06-24 01:18:41 +08:00
MaysWind 69f5aca853 fix autocomplete style issue 2023-06-24 00:16:19 +08:00
MaysWind 8e6aece9ae code refactor 2023-06-23 22:23:09 +08:00
MaysWind 59b883ff7f modify style 2023-06-23 21:16:17 +08:00
MaysWind 548d34fbf4 add tooltip 2023-06-23 18:00:31 +08:00
MaysWind bb6345ccfa update vue-datepicker, and make the picker show the calendar view every time opening 2023-06-23 15:48:47 +08:00
MaysWind d59a10f718 fix wrong style 2023-06-23 15:00:38 +08:00
MaysWind aab1c5419a remove unused reference 2023-06-23 14:53:22 +08:00
MaysWind 9b83785b95 modify style 2023-06-23 14:51:07 +08:00
MaysWind 099b710eb1 code refactor 2023-06-23 14:16:37 +08:00
MaysWind a5424afc38 modify style 2023-06-23 13:40:15 +08:00
MaysWind 626325066d code refactor 2023-06-23 13:38:09 +08:00
MaysWind 37195f6008 code refactor 2023-06-23 10:04:33 +08:00
MaysWind fcbc68cefe modify file name 2023-06-23 10:00:05 +08:00
MaysWind 9241953e31 modify style 2023-06-23 01:37:58 +08:00
MaysWind 651a912498 support change theme 2023-06-23 01:24:10 +08:00
MaysWind a05f6fb6b5 modify style 2023-06-23 00:59:14 +08:00
MaysWind d6440d31f2 add exchange rates page 2023-06-23 00:21:00 +08:00
MaysWind 88d5dc2f17 set default timeout setting 2023-06-22 22:54:48 +08:00
MaysWind a17ad85858 add about page 2023-06-22 21:48:19 +08:00
MaysWind 4b49c1f30f add desktop frontend framework 2023-06-22 21:30:18 +08:00
MaysWind a9e36b9a59 modify style 2023-06-22 18:43:39 +08:00
MaysWind 80429bbfb8 code refactor 2023-06-22 16:35:52 +08:00
MaysWind f39e20d7a7 support setting user disabled 2023-06-21 23:57:04 +08:00
MaysWind a03bac5d74 modify style 2023-06-21 23:38:37 +08:00
MaysWind 1aff09598a code refactor 2023-06-21 23:25:56 +08:00
MaysWind 4036a71ee1 update third party dependencies 2023-06-21 22:01:23 +08:00
MaysWind a966be2f46 package framework7 and related dependencies into vendor-mobile.js file 2023-06-21 21:52:31 +08:00
MaysWind a0b3bc1cab fix default text size 2023-06-21 21:50:16 +08:00
MaysWind eb2e2b0a26 show whether data is updated after pull down 2023-06-19 00:12:39 +08:00
MaysWind 55ad7b2e81 code refactor 2023-06-18 21:07:44 +08:00
MaysWind dbcd2897a4 support tomtom map 2023-06-18 21:02:55 +08:00
MaysWind 68a6d1c166 update configuration comment 2023-06-18 20:40:45 +08:00
MaysWind fe82ec6fc2 support OpenStreetMap(Humanitarian), OpenTopoMap, OPNVKarte, CyclOSM 2023-06-18 18:24:11 +08:00
MaysWind 5f2819a961 code refactor 2023-06-18 16:30:19 +08:00
MaysWind 812bfc7cf5 set reference libraries in google map js query 2023-06-18 16:29:30 +08:00
MaysWind d164cafd33 fix npe 2023-06-18 15:53:06 +08:00
MaysWind 3ada4183d9 code refactor 2023-06-18 15:51:57 +08:00
MaysWind fa68621b41 support api proxy for amap 2023-06-18 15:43:27 +08:00
MaysWind 4f2b9d39da code refactor 2023-06-18 09:20:02 +08:00
MaysWind fd01c9269f improve robustness 2023-06-18 01:25:47 +08:00
MaysWind 82d150e53a support amap 2023-06-18 01:24:29 +08:00
MaysWind 251f3fe2da update config comment 2023-06-18 00:26:18 +08:00
MaysWind 4f0e1e6b3d set position and zoom level when init map 2023-06-18 00:06:51 +08:00
MaysWind a5dbf5d4b7 support google map 2023-06-17 23:24:45 +08:00
MaysWind 38baf77c30 support get current language info 2023-06-17 23:07:22 +08:00
MaysWind bfb8b03fc9 add language code 2023-06-17 23:07:11 +08:00
MaysWind 2dd38d9b03 force set default language when specified language not exists, force set locale settings when first set locale settings 2023-06-17 23:04:46 +08:00
MaysWind 307c64bc1e don't write unnecessary info to cookies 2023-06-17 19:44:21 +08:00
MaysWind 782e3a85f9 support baidu map 2023-06-17 19:38:13 +08:00
MaysWind 3bae6e749a support baidu map 2023-06-17 17:47:12 +08:00
MaysWind 530ef6b83e fix default text size is not set to default 2023-06-17 13:09:39 +08:00
MaysWind 5b334eb2d5 show time difference between the transaction timezone and the default timezone on the transaction edit/view page 2023-06-15 01:31:29 +08:00
MaysWind 28dc2e425a fix the problem the end time not equals to the current time in transaction list page when timezone not set to browser timezone 2023-06-15 01:27:15 +08:00
mayswind 83b72e7403 remove duplicated code 2023-06-14 09:12:50 +08:00
MaysWind 01e1f65ffe display preview when drag the range slider 2023-06-14 01:38:52 +08:00
MaysWind 9b15f888e6 improve tree view selection sheet style 2023-06-14 01:36:27 +08:00
MaysWind 171b8afa8e improve text size settings 2023-06-14 01:27:23 +08:00
MaysWind 27f2f9f13a modify transaction edit page loading style 2023-06-14 00:45:49 +08:00
MaysWind 2c3983cead modify transaction list page loading style 2023-06-14 00:45:33 +08:00
MaysWind bebd043d58 add font settings page 2023-06-13 01:33:54 +08:00
MaysWind a1c828fe62 add more supported font size 2023-06-13 01:19:42 +08:00
MaysWind dfb885f38d support setting app font size 2023-06-12 01:39:23 +08:00
MaysWind 702c095544 fix the problem that the last label does not have divider line when filling in a new label 2023-06-11 22:55:12 +08:00
MaysWind b3e886d444 fix npe error 2023-06-11 22:53:32 +08:00
MaysWind 46d85e92cd use pinia to replace vuex, code refactor 2023-06-11 22:08:30 +08:00
MaysWind 0d84f2857f update README 2023-06-10 22:13:54 +08:00
MaysWind ac6c80db90 bump version to 0.4.0 2023-06-10 18:03:03 +08:00
MaysWind e608e85d56 support disable map 2023-06-10 17:14:58 +08:00
MaysWind 58104a9a4d append thousands separator in data management page 2023-06-10 17:04:35 +08:00
MaysWind 8d68dcabb5 list item selection sheet supports large size 2023-06-06 01:01:24 +08:00
MaysWind a5e5389d6c fix some text not changed when user language has been changed 2023-06-06 00:56:39 +08:00
MaysWind 8de862c82f optimize the style when the text is too long 2023-06-06 00:52:35 +08:00
MaysWind 5141303ee1 support turning on dark theme manually 2023-06-05 23:54:01 +08:00
MaysWind 3d95680cbc remove unused code 2023-06-05 09:47:09 +08:00
MaysWind 78663b873c support only match the first locale part when get browser language 2023-06-05 00:56:04 +08:00
MaysWind 0a106026dd user settings supports language and date&time format 2023-06-05 00:54:07 +08:00
MaysWind 999ca6274c remove unused file 2023-06-04 21:08:38 +08:00
MaysWind 36b9177069 fix building issue 2023-06-04 15:43:38 +08:00
MaysWind 8cf7bf859b support map provider and whether use map data proxy settings 2023-06-04 15:06:14 +08:00
MaysWind 2e54b62f60 set cookies in development mode 2023-06-04 14:25:51 +08:00
MaysWind df46069d92 code refactor 2023-06-04 13:15:09 +08:00
MaysWind e31014dde4 code refactor 2023-06-04 13:04:15 +08:00
MaysWind f9a14581e1 fix typo 2023-06-04 01:24:08 +08:00
MaysWind 95ec005894 add icons 2023-06-04 01:22:43 +08:00
MaysWind 73d271b8bc improve ui 2023-06-04 01:22:33 +08:00
MaysWind 0c3b56e44a set the color of input and confirm button to red in password input sheet when clear user data 2023-06-03 20:11:03 +08:00
MaysWind 736f340979 code refactor 2023-06-03 16:49:19 +08:00
MaysWind 49e62d35c3 exchange rate datasource supports Monetary Authority of Singapore 2023-05-29 01:04:16 +08:00
MaysWind 810bce7495 update latest available currencies 2023-05-29 00:27:30 +08:00
MaysWind aff876aa05 only trigger gitea docker-snapshot workflow when push to main branch 2023-05-28 17:59:25 +08:00
MaysWind 9511644ce6 update third party dependencies 2023-05-28 17:59:08 +08:00
MaysWind 21d73e5f69 update golang to 1.20.4, node to 18.16.0, base docker image to alpine 3.17.3 2023-05-28 12:18:40 +08:00
MaysWind d62f3fb936 update github actions 2023-05-21 00:06:11 +08:00
MaysWind 0a011b6075 add gitea actions 2023-05-20 23:49:00 +08:00
MaysWind 4e561dc764 change map marker icon 2023-05-15 23:31:53 +08:00
MaysWind a55256ad82 code refactor 2023-05-15 22:36:21 +08:00
MaysWind ab26ef64a5 code refactor 2023-05-15 22:35:59 +08:00
mayswind 6d1610eee0 fix wrong proxy path 2023-05-15 00:49:02 +08:00
MaysWind 2ba143d6ea support showing geolocation on map 2023-05-15 00:08:03 +08:00
MaysWind bd542ac308 modify geolocation storage type in database 2023-05-14 19:42:57 +08:00
MaysWind e71ffd1a77 support storing geo location in transaction 2023-04-29 13:45:19 +08:00
MaysWind 1ac968f63c improve ui 2023-04-28 17:21:27 +08:00
MaysWind dc4f62a085 modify Simplified Chinese translation 2023-04-28 16:26:52 +08:00
MaysWind ad91d4f1ce add show/hide hidden transaction category/tag menu 2023-04-28 16:24:15 +08:00
MaysWind 8d4c7512ab reduce unit test execute times 2023-04-24 00:54:24 +08:00
MaysWind 12d5837526 modify unit test case fail cause message 2023-04-23 21:49:27 +08:00
MaysWind c82d2abab4 fix problem that cannot switch to other date range sometimes in transaction list page 2023-04-23 01:27:38 +08:00
MaysWind 26cb717a08 build when push to non-main branch 2023-04-23 01:00:27 +08:00
MaysWind 0b1cc0ef5b add lint checking and unit testing in build script 2023-04-23 00:45:56 +08:00
MaysWind 05dc9138b4 fix problem that cannot get transaction statistics when db is set to only_full_group_by 2023-04-23 00:03:26 +08:00
MaysWind a7dcacb26c add log 2023-04-22 23:54:57 +08:00
MaysWind 3ac3f871e4 fix the problem that account list page does not update when modify category of account 2023-04-22 23:12:41 +08:00
MaysWind b180c0bbe6 modify link text 2023-04-22 22:35:36 +08:00
MaysWind e4987f3bde modify vue page name 2023-04-22 22:28:04 +08:00
MaysWind ee2690c2cc fix the problem that cannot change language in transaction preset category page 2023-04-22 21:50:08 +08:00
MaysWind 4cad26f793 change moment api 2023-04-22 21:26:42 +08:00
MaysWind 84f3d5fec5 remove unused code 2023-04-22 21:09:01 +08:00
MaysWind bb3939d570 show none when user does not have visible account or category 2023-04-22 21:03:41 +08:00
MaysWind b303f708f5 improve ui 2023-04-22 20:52:20 +08:00
MaysWind d39550e090 improve ui 2023-04-22 20:47:25 +08:00
MaysWind 22d956f38a improve ui 2023-04-22 20:24:53 +08:00
MaysWind dab7728138 update prompt text 2023-04-22 17:29:30 +08:00
MaysWind 4038b6bc51 improve ui 2023-04-22 17:00:56 +08:00
MaysWind bcaf3a246c show no available category / account in transaction statistics filter page when there are no available category or account 2023-04-22 16:19:10 +08:00
MaysWind 4fb115fcbc remove unused code 2023-04-22 01:37:16 +08:00
MaysWind bfd4b2b6de code refactor 2023-04-22 01:33:38 +08:00
MaysWind c0cd3fc5c2 improve ui 2023-04-22 01:24:49 +08:00
MaysWind f95c4393d2 fix the problem that the day of week in date time / date range picker is wrong 2023-04-22 01:10:22 +08:00
MaysWind e2eb5fabcc improve ui 2023-04-22 00:49:47 +08:00
MaysWind 7275e8ff0d modify infinite distance 2023-04-22 00:31:19 +08:00
MaysWind accfc3df12 improve ui 2023-04-21 23:56:52 +08:00
MaysWind 35392483d9 code refactor 2023-04-21 23:56:44 +08:00
MaysWind 9770851fd4 fix the problem that page jump infinitely 2023-04-21 23:56:02 +08:00
MaysWind e013a6f087 fix the display amount in statistics page when there are no transaction 2023-04-21 23:53:19 +08:00
MaysWind 1ec9ff20b1 remove unused code 2023-04-21 23:41:53 +08:00
MaysWind 9b0dea80c9 don't allow clear the value in datetime picker 2023-04-21 23:14:26 +08:00
MaysWind eea1ea7ed0 always show date range picker in center 2023-04-21 22:36:57 +08:00
MaysWind 85cd46bfc7 fix problem the category separate icon in transaction page does not display 2023-04-21 22:30:07 +08:00
MaysWind a7ca394864 code refactor 2023-04-21 22:20:11 +08:00
MaysWind e178a0795a code refactor 2023-04-21 22:16:35 +08:00
MaysWind e8b0470ceb code refactor 2023-04-21 22:11:10 +08:00
MaysWind c08abfdfdf fix frontend build issue 2023-04-21 08:47:15 +08:00
mayswind b1c765eb51 Upgrade to vue3 (#16)
* upgrade to vue 3.x and framework7 8.x
* change calendar plugin to vue-datepicker
* disable export button when user does not hava any transaction
* implement new pin code input
* append thousands separator in amount in exchange rates page
2023-04-21 01:45:00 +08:00
mayswind 4b0f7d45e8 Merge pull request #15 from vigdail/bugfix/atomic_alignment
Proper fields alignment in `InternalUuidGenerator` struct
2023-04-20 22:49:49 +08:00
vigdail 9e6271b1dc Rearrange fields of InternalUuidGenerator struct to fit atomic alignment requirements 2023-04-20 19:39:38 +06:00
MaysWind a96eb31dc9 redirect to login page when user logout without token 2023-04-16 02:00:59 +08:00
MaysWind 221a7809b6 fix npe error 2023-04-12 00:24:20 +08:00
MaysWind 8c33243c90 code refactor 2023-04-09 01:04:00 +08:00
MaysWind c4b07b98aa update third party dependencies 2023-04-08 14:53:26 +08:00
MaysWind 1fda80a86b bump version to 0.3.0 2023-04-08 14:49:39 +08:00
MaysWind 1287c729f2 modify font color style in transaction view page 2023-04-03 00:29:25 +08:00
MaysWind c069faa6f4 modify item header color in ios dark theme 2023-04-03 00:25:50 +08:00
MaysWind 286fd91b2b modify setting ui 2023-04-02 23:29:41 +08:00
MaysWind 33250d2f3d optimize user data export process 2023-04-02 23:18:05 +08:00
MaysWind 44ca940ca3 add splash screen images for ios 2023-04-02 21:05:59 +08:00
MaysWind 3b0ef7a96d data management page shows all user data statistics 2023-04-02 19:36:10 +08:00
MaysWind dfb6c593e4 format code 2023-04-02 18:14:16 +08:00
MaysWind 853b01e2ca show message when force update exchange rates data and the data is up to date 2023-04-02 18:06:36 +08:00
MaysWind 5a924fa382 code refactor 2023-04-02 17:35:31 +08:00
MaysWind d4985a024d use unambiguous numeric variable type 2023-03-27 23:58:33 +08:00
MaysWind 2797266de6 check numeric setting value, add numeric value range comment in config file 2023-03-27 23:13:18 +08:00
MaysWind b476cc91ca bump year 2023-03-27 22:10:57 +08:00
MaysWind 9e3aa19a09 make related transaction has the same unix time with the original transaction 2023-03-27 00:11:39 +08:00
MaysWind 7443e8a532 uuid generator supports generating more than 1 uuid in one time 2023-03-27 00:02:48 +08:00
MaysWind 1d6dbf63c0 code refactor 2023-03-26 23:53:52 +08:00
MaysWind e17687f80d fix wrong string format placeholder 2023-03-26 23:24:29 +08:00
MaysWind 27f4e14a4e code refactor 2023-03-26 22:24:26 +08:00
MaysWind 8d5de98218 record transaction created ip 2023-03-26 22:10:04 +08:00
MaysWind dbf5c0a5bd update golang to 1.20 and update nodejs to 18.15 2023-03-24 00:45:33 +08:00
MaysWind c1422f789a update third party dependencies 2023-03-24 00:40:46 +08:00
MaysWind 613af9399a update badge image 2023-01-08 21:57:22 +08:00
MaysWind 34a6108bb2 modify the end time of last 7/30 days to the end time in today 2023-01-05 22:34:45 +08:00
MaysWind 66a5508abe update third party dependencies 2022-12-06 01:16:02 +08:00
MaysWind 3a273ea64f update base image 2022-12-06 01:14:39 +08:00
MaysWind 6f88e6ef26 add health check api 2022-12-05 22:59:26 +08:00
MaysWind fd7905833e optimize statistics page style 2022-08-01 00:33:19 +08:00
MaysWind 1977e436d6 add "sort by" drop down list in statistics page 2022-07-25 01:11:35 +08:00
MaysWind 0dfb3d00e9 code refactor 2022-07-25 00:22:04 +08:00
MaysWind 785ec9bdb1 update docker base image 2022-07-24 20:35:55 +08:00
MaysWind eeccc4fd49 update third party dependencies 2022-07-24 20:14:39 +08:00
MaysWind efea9a7c37 update year 2022-07-24 19:20:40 +08:00
MaysWind 04eafd6705 code refactor 2022-07-24 19:00:16 +08:00
MaysWind 73b554aa48 auto set transaction type in transaction adding page according to the category id 2022-07-24 17:56:49 +08:00
MaysWind d3ddcf4c20 bump version to 0.2.0 2022-07-24 15:45:12 +08:00
MaysWind ed9ea2f1d3 modify cancel button text and position in license popup 2022-07-21 01:10:36 +08:00
MaysWind 7eae3e9923 show currency code when show currency name 2022-07-21 01:07:43 +08:00
MaysWind 03d95033d7 allow set base amount in exchange rate page 2022-07-21 00:41:37 +08:00
MaysWind 9a79606565 support set user default account 2022-04-18 00:16:47 +08:00
MaysWind c5a101aad2 fix bug 2022-04-17 23:32:00 +08:00
MaysWind 2dbf4d652d update third party dependencies 2022-03-20 23:20:11 +08:00
MaysWind 7364380312 support filter by parent account in transaction list page 2022-03-20 22:26:27 +08:00
MaysWind b7fe70aba3 change method name 2022-03-07 00:27:12 +08:00
MaysWind bca9982c57 hide add icon when filter parent account in transaction list page 2022-03-07 00:02:00 +08:00
MaysWind 7c16435010 make parent account clickable in account list page 2022-03-06 23:55:18 +08:00
MaysWind a79def625c update third party dependencies 2022-03-06 22:59:24 +08:00
MaysWind 8d5f804a60 modify save button text 2022-01-03 20:25:56 +08:00
mayswind 0167381f0d Merge pull request #1 from jiangshengwu/fix_transaction_amount
remove uid in selected columns
2021-07-12 20:31:15 +08:00
Shengwu Jiang 4e2c4b39bb remove uid in selected columns 2021-07-12 20:18:00 +08:00
MaysWind 7bfc84abc8 update comment 2021-07-04 22:57:28 +08:00
MaysWind 5ac6c64079 add data & log folder for building package 2021-06-29 23:57:21 +08:00
MaysWind e7c4261b86 add generating secret key utility 2021-06-28 00:41:14 +08:00
MaysWind 163b75e81b modify method name 2021-06-28 00:39:12 +08:00
MaysWind 949132ef5a modify timezone 2021-06-27 23:15:10 +08:00
MaysWind 5617d31ed8 update build.sh help message 2021-06-23 00:13:17 +08:00
MaysWind 832d865397 modify comments in config file 2021-06-21 00:54:14 +08:00
507 changed files with 75273 additions and 32369 deletions
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
'root': true,
'env': {
'node': true
},
'extends': [
'eslint:recommended',
'plugin:vue/vue3-essential'
],
'rules': {
'vue/no-use-v-if-with-v-for': 'off'
}
}
+56
View File
@@ -0,0 +1,56 @@
name: Docker Release
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set up the environment
run: |
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 }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_RELEASE_PLATFORMS }}
push: true
build-args: |
RELEASE_BUILD=1
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+54
View File
@@ -0,0 +1,54 @@
name: Docker Snapshot
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
type=sha,format=short,prefix=SNAPSHOT-
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set up the environment
run: |
sed -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 }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+32 -23
View File
@@ -9,35 +9,44 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up docker tag
id: vars
run: echo ::set-output name=RELEASE_TAG::${GITHUB_REF/refs\/tags\/v/}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
platforms: |
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/arm/v6
push: true
build-args: |
RELEASE_BUILD=1
tags: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:${{ steps.vars.outputs.RELEASE_TAG }}
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:latest
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+31 -23
View File
@@ -9,33 +9,41 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up docker tag
id: vars
run: echo ::set-output name=BUILD_DATE::$(date '+%Y%m%d')
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
platforms: |
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/arm/v6
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:SNAPSHOT-${{ steps.vars.outputs.BUILD_DATE }}
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:latest-snapshot
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -0,0 +1,28 @@
name: Docker Snapshot
on:
push:
branches-ignore:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build
uses: docker/build-push-action@v2
with:
file: Dockerfile
context: .
platforms: linux/amd64
push: false
+6 -4
View File
@@ -1,5 +1,5 @@
# Build backend binary file
FROM golang:1.16.5-alpine3.13 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:14.17.0-alpine3.13 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.13.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,11 +27,13 @@ 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
COPY --chown=1000:1000 conf /ezbookkeeping/conf
COPY --chown=1000:1000 templates /ezbookkeeping/templates
COPY --chown=1000:1000 LICENSE /ezbookkeeping/LICENSE
USER 1000:1000
EXPOSE 8080
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2021 MaysWind (i@mayswind.net)
Copyright (c) 2020-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
+30 -7
View File
@@ -1,6 +1,6 @@
# ezBookkeeping
[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
[![Latest Build](https://img.shields.io/github/workflow/status/mayswind/ezbookkeeping/Docker%20Release?style=flat)](https://github.com/mayswind/ezbookkeeping/actions)
[![Latest Build](https://img.shields.io/github/actions/workflow/status/mayswind/ezbookkeeping/docker-snapshot.yml?branch=main)](https://github.com/mayswind/ezbookkeeping/actions)
[![Go Report](https://goreportcard.com/badge/github.com/mayswind/ezbookkeeping)](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
[![Latest Docker Image Size](https://img.shields.io/docker/image-size/mayswind/ezbookkeeping.svg?style=flat)](https://hub.docker.com/r/mayswind/ezbookkeeping)
[![Latest Release](https://img.shields.io/github/release/mayswind/ezbookkeeping.svg?style=flat)](https://github.com/mayswind/ezbookkeeping/releases)
@@ -8,17 +8,21 @@
## 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
3. Easy to install
* Docker support
* Multiple database support (sqlite, mysql, etc.)
* Multiple os & architecture support (Windows, macOS, Linux & x86, amd64, ARM)
* Multiple database support (SQLite, MySQL, PostgreSQL, etc.)
* Multiple operation system & hardware support (Windows, macOS, Linux & x86, amd64, ARM)
4. User-friendly interface
* Both desktop and mobile UI
* Close to native app experience (for mobile device)
* Two-level account & two-level category support
* Plentiful preset categories
* Geographic location and map support
* Searching & filtering history records
* Data statistics
* Dark theme
@@ -26,12 +30,15 @@ ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It c
6. Multiple timezone support
7. Multi-language support
8. Two-factor authentication
9. Application lock (WebAuthn support)
9. Application lock (PIN code / WebAuthn)
10. Data export
## Screenshots
### Mobile Device
[![ezBookkeeping](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/en.png)](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/en.png)
### Desktop Version
[![ezBookkeeping](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/desktop/en.png)](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/desktop/en.png)
### Mobile Version
[![ezBookkeeping](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/en.png)](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/en.png)
## Installation
### Ship with docker
@@ -48,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
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+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
+62 -15
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env sh
TYPE=""
NO_LINT="0"
NO_TEST="0"
RELEASE=${RELEASE_BUILD:-"0"}
RELEASE_TYPE="unknown"
VERSION=""
@@ -31,16 +33,18 @@ Usage:
build.sh type [options]
Types:
backend Build backend binary file
frontend Build frontend files
package Build package archive
docker Build docker image
backend Build backend binary file
frontend Build frontend files
package Build package archive
docker Build docker image
Options:
-r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
-o, --output Package file name (For "package" type only)
-t, --tag Docker tag (For "docker" type only)
-h, --help Show help
-r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
-o, --output <filename> Package file name (For "package" type only)
-t, --tag Docker tag (For "docker" type only)
--no-lint Do not execute lint check before building
--no-test Do not execute unit testing before building
-h, --help Show help
EOF
}
@@ -63,6 +67,12 @@ parse_args() {
DOCKER_TAG="$2"
shift
;;
--no-lint)
NO_LINT="1"
;;
--no-test)
NO_TEST="1"
;;
--help | -h)
show_help
exit 0
@@ -111,6 +121,30 @@ set_build_parameters() {
}
build_backend() {
echo "Pulling backend dependencies..."
go get .
if [ "$NO_LINT" = "0" ]; then
echo "Executing backend lint checking..."
go vet -v ./...
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass lint checking"
exit 1
fi
fi
if [ "$NO_TEST" = "0" ]; then
echo "Executing backend unit testing..."
go clean -cache
go test ./... -v
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass unit testing"
exit 1
fi
fi
backend_build_extra_arguments="-X main.Version=$VERSION"
backend_build_extra_arguments="$backend_build_extra_arguments -X main.CommitHash=$COMMIT_HASH"
@@ -125,17 +159,26 @@ build_backend() {
}
build_frontend() {
frontend_build_arguments="";
if [ "$RELEASE" = "0" ]; then
frontend_build_arguments="--buildUnixTime=$BUILD_UNIXTIME"
fi
echo "Pulling frontend dependencies..."
npm install
if [ "$NO_LINT" = "0" ]; then
echo "Executing frontend lint checking..."
npm run lint
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass lint checking"
exit 1
fi
fi
echo "Building frontend files ($RELEASE_TYPE)..."
npm run build -- "$frontend_build_arguments"
if [ "$RELEASE" = "0" ]; then
buildUnixTime=$BUILD_UNIXTIME npm run build
else
npm run build
fi
}
build_package() {
@@ -158,9 +201,13 @@ 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
}
+70 -11
View File
@@ -7,9 +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"
)
@@ -17,64 +20,117 @@ import (
func initializeSystem(c *cli.Context) (*settings.Config, error) {
var err error
configFilePath := c.String("conf-path")
isDisableBootLog := c.Bool("no-boot-log")
if configFilePath != "" {
if _, err = os.Stat(configFilePath); err != nil {
log.BootErrorf("[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath)
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath)
}
return nil, err
}
log.BootInfof("[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath)
if !isDisableBootLog {
log.BootInfof("[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath)
}
} else {
configFilePath, err = settings.GetDefaultConfigFilePath()
if err != nil {
log.BootErrorf("[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error())
}
return nil, err
}
log.BootInfof("[initializer.initializeSystem] will load configuration from default config path %s", configFilePath)
if !isDisableBootLog {
log.BootInfof("[initializer.initializeSystem] will load configuration from default config path %s", configFilePath)
}
}
config, err := settings.LoadConfiguration(configFilePath)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] cannot load configuration, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] cannot load configuration, because %s", err.Error())
}
return nil, err
}
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)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] initializes data store failed, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes data store failed, because %s", err.Error())
}
return nil, err
}
err = log.SetLoggerConfiguration(config)
err = log.SetLoggerConfiguration(config, isDisableBootLog)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error())
}
return nil, err
}
err = 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 {
log.BootErrorf("[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error())
}
return nil, err
}
err = 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 {
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes mailer failed, because %s", err.Error())
}
return nil, err
}
err = exchangerates.InitializeExchangeRatesDataSource(config)
if err != nil {
log.BootErrorf("[initializer.initializeSystem] initializes exchange rates data source failed, because %s", err.Error())
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes exchange rates data source failed, because %s", err.Error())
}
return nil, err
}
cfgJson, _ := json.Marshal(getConfigWithoutSensitiveData(config))
log.BootInfof("[initializer.initializeSystem] has loaded configuration %s", cfgJson)
if !isDisableBootLog {
log.BootInfof("[initializer.initializeSystem] has loaded configuration %s", cfgJson)
}
return config, nil
}
@@ -88,7 +144,10 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
}
clonedConfig.DatabaseConfig.DatabasePassword = "****"
clonedConfig.SMTPConfig.SMTPPasswd = "****"
clonedConfig.MinIOConfig.SecretAccessKey = "****"
clonedConfig.SecretKey = "****"
clonedConfig.AmapApplicationSecret = "****"
return clonedConfig
}
+49
View File
@@ -0,0 +1,49 @@
package cmd
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// SecurityUtils represents the security command
var SecurityUtils = &cli.Command{
Name: "security",
Usage: "ezBookkeeping security utilities",
Subcommands: []*cli.Command{
{
Name: "gen-secret-key",
Usage: "Generate a random secret key",
Action: genSecretKey,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "length",
Aliases: []string{"l"},
Required: false,
DefaultText: "32",
Usage: "The length of secret key",
},
},
},
},
}
func genSecretKey(c *cli.Context) error {
length := c.Int("length")
if length <= 0 {
length = 32
}
secretKey, err := utils.GetRandomNumberOrLetter(length)
if err != nil {
return err
}
fmt.Printf("[Secret Key] %s\n", secretKey)
return nil
}
+268 -8
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"
)
@@ -86,6 +87,71 @@ var UserData = &cli.Command{
},
},
},
{
Name: "user-enable",
Usage: "Enable specified user",
Action: enableUser,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-disable",
Usage: "Disable specified user",
Action: disableUser,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-resend-verify-email",
Usage: "Resend user verify email",
Action: resendUserVerifyEmail,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-set-email-verified",
Usage: "Set user email address verified",
Action: setUserEmailVerified,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-set-email-unverified",
Usage: "Set user email address unverified",
Action: setUserEmailUnverified,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "user-delete",
Usage: "Delete specified user",
@@ -138,6 +204,19 @@ var UserData = &cli.Command{
},
},
},
{
Name: "send-password-reset-mail",
Usage: "Send password reset mail",
Action: sendPasswordResetMail,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "transaction-check",
Usage: "Check whether user all transactions and accounts are correct",
@@ -151,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{
@@ -168,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",
},
},
},
},
@@ -239,6 +337,126 @@ func modifyUserPassword(c *cli.Context) error {
return nil
}
func sendPasswordResetMail(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.SendPasswordResetMail(c, username)
if err != nil {
log.BootErrorf("[user_data.sendPasswordResetMail] error occurs when sending password reset email")
return err
}
log.BootInfof("[user_data.sendPasswordResetMail] a password reset email for user \"%s\" has been sent", username)
return nil
}
func enableUser(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.EnableUser(c, username)
if err != nil {
log.BootErrorf("[user_data.enableUser] error occurs when setting user enabled")
return err
}
log.BootInfof("[user_data.enableUser] user \"%s\" has been set enabled", username)
return nil
}
func disableUser(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.DisableUser(c, username)
if err != nil {
log.BootErrorf("[user_data.disableUser] error occurs when setting user disabled")
return err
}
log.BootInfof("[user_data.disableUser] user \"%s\" has been set disabled", username)
return nil
}
func resendUserVerifyEmail(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.ResendVerifyEmail(c, username)
if err != nil {
log.BootErrorf("[user_data.resendUserVerifyEmail] error occurs when resending user verify email")
return err
}
log.BootInfof("[user_data.resendUserVerifyEmail] verify email for user \"%s\" has been resent", username)
return nil
}
func setUserEmailVerified(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.SetUserEmailVerified(c, username)
if err != nil {
log.BootErrorf("[user_data.setUserEmailVerified] error occurs when setting user email address verified")
return err
}
log.BootInfof("[user_data.setUserEmailVerified] user \"%s\" email address has been set verified", username)
return nil
}
func setUserEmailUnverified(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
err = clis.UserData.SetUserEmailUnverified(c, username)
if err != nil {
log.BootErrorf("[user_data.setUserEmailUnverified] error occurs when setting user email address unverified")
return err
}
log.BootInfof("[user_data.setUserEmailUnverified] user \"%s\" email address has been set unverified", username)
return nil
}
func deleteUser(c *cli.Context) error {
_, err := initializeSystem(c)
@@ -270,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
}
@@ -348,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)
@@ -357,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
}
@@ -372,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")
@@ -398,9 +645,21 @@ func printUserInfo(user *models.User) {
fmt.Printf("[Nickname] %s\n", user.Nickname)
fmt.Printf("[Password] %s\n", user.Password)
fmt.Printf("[Salt] %s\n", user.Salt)
fmt.Printf("[DefaultAccountId] %d\n", user.DefaultAccountId)
fmt.Printf("[TransactionEditScope] %s (%d)\n", user.TransactionEditScope, user.TransactionEditScope)
fmt.Printf("[Language] %s\n", user.Language)
fmt.Printf("[DefaultCurrency] %s\n", user.DefaultCurrency)
fmt.Printf("[FirstDayOfWeek] %s\n", user.FirstDayOfWeek)
fmt.Printf("[TransactionEditScope] %s\n", user.TransactionEditScope)
fmt.Printf("[FirstDayOfWeek] %s (%d)\n", user.FirstDayOfWeek, user.FirstDayOfWeek)
fmt.Printf("[LongDateFormat] %s (%d)\n", user.LongDateFormat, user.LongDateFormat)
fmt.Printf("[ShortDateFormat] %s (%d)\n", user.ShortDateFormat, user.ShortDateFormat)
fmt.Printf("[LongTimeFormat] %s (%d)\n", user.LongTimeFormat, user.LongTimeFormat)
fmt.Printf("[ShortTimeFormat] %s (%d)\n", user.ShortTimeFormat, user.ShortTimeFormat)
fmt.Printf("[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)
@@ -421,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)
}
+129
View File
@@ -0,0 +1,129 @@
package cmd
import (
"encoding/binary"
"fmt"
"net"
"github.com/urfave/cli/v2"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/requestid"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// Utilities represents the utilities command
var Utilities = &cli.Command{
Name: "utility",
Usage: "ezBookkeeping utilities",
Subcommands: []*cli.Command{
{
Name: "parse-default-request-id",
Usage: "Parse a request id which is generated by default request generator and show the details",
Action: parseRequestId,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "id",
Required: true,
Usage: "Request ID",
},
},
},
{
Name: "send-test-mail",
Usage: "Send an email to specified e-mail address",
Action: sendTestMail,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "to",
Required: true,
Usage: "To e-mail address",
},
},
},
},
}
func parseRequestId(c *cli.Context) error {
config, err := initializeSystem(c)
if err != nil {
return err
}
err = requestid.InitializeRequestIdGenerator(config)
defaultGenerator, err := requestid.NewDefaultRequestIdGenerator(config)
if err != nil {
return err
}
requestId := c.String("id")
requestIdInfo, err := defaultGenerator.ParseRequestIdInfo(requestId)
if err != nil {
return err
}
newRequestId := defaultGenerator.GenerateRequestId(net.IPv4zero.String(), 0)
newRequestIdInfo, err := defaultGenerator.ParseRequestIdInfo(newRequestId)
printRequestIdInfo(requestId, requestIdInfo, newRequestIdInfo)
return nil
}
func sendTestMail(c *cli.Context) error {
config, err := initializeSystem(c)
if err != nil {
return err
}
if !config.EnableSMTP || mail.Container.Current == nil {
return errs.ErrSMTPServerNotEnabled
}
toAddress := c.String("to")
err = mail.Container.Current.SendMail(&mail.MailMessage{
To: toAddress,
Subject: "ezBookkeeping test e-mail",
Body: "This is a test e-mail",
})
if err != nil {
return err
}
fmt.Printf("Test e-mail has been sent")
return nil
}
func printRequestIdInfo(requestId string, requestIdInfo *requestid.RequestIdInfo, newRequestIdInfo *requestid.RequestIdInfo) {
fmt.Printf("[RequestId] %s\n", requestId)
fmt.Printf("[ServerUniqId] %d (Current Server %d)\n", requestIdInfo.ServerUniqId, newRequestIdInfo.ServerUniqId)
fmt.Printf("[InstanceUniqId] %d (Current Server %d)\n", requestIdInfo.InstanceUniqId, newRequestIdInfo.InstanceUniqId)
displayTime, err := utils.ParseFromElapsedSeconds(int(requestIdInfo.SecondsElapsedToday))
if err == nil {
fmt.Printf("[SecondsElapsedToday] %d (%s)\n", requestIdInfo.SecondsElapsedToday, displayTime)
} else {
fmt.Printf("[SecondsElapsedToday] %d\n", requestIdInfo.SecondsElapsedToday)
}
fmt.Printf("[RequestSeqId] %d\n", requestIdInfo.RequestSeqId)
fmt.Printf("[IsClientIpv6] %t\n", requestIdInfo.IsClientIpv6)
if requestIdInfo.IsClientIpv6 {
fmt.Printf("[ClientIpv6Hash] %d\n", requestIdInfo.ClientIp)
} else {
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, requestIdInfo.ClientIp)
fmt.Printf("[ClientIpv4] %s\n", ip.String())
}
fmt.Printf("[ClientPort] %d\n", requestIdInfo.ClientPort)
}
+195 -24
View File
@@ -3,7 +3,10 @@ package cmd
import (
"fmt"
"path/filepath"
"time"
"github.com/gin-contrib/cache"
"github.com/gin-contrib/cache/persistence"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
@@ -41,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
}
}
@@ -55,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
}
@@ -65,12 +68,14 @@ 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)
}
workboxFileNames := utils.ListFileNamesWithPrefixAndSuffix(config.StaticRootPath, "workbox-", ".js")
router := gin.New()
router.Use(bindMiddleware(middlewares.Recovery))
@@ -84,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))
@@ -110,13 +116,16 @@ func startWebServer(c *cli.Context) error {
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
router.Static("/mobile/fonts", filepath.Join(config.StaticRootPath, "fonts"))
router.Static("/mobile/sw", filepath.Join(config.StaticRootPath, "sw"))
router.StaticFile("/mobile/favicon.ico", filepath.Join(config.StaticRootPath, "favicon.ico"))
router.StaticFile("/mobile/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
router.StaticFile("/mobile/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
for i := 0; i < len(workboxFileNames); i++ {
router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
desktopEntryRoute := router.Group("/desktop")
desktopEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
{
@@ -130,37 +139,106 @@ func startWebServer(c *cli.Context) error {
router.StaticFile("/desktop/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
router.StaticFile("/desktop/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
router.StaticFile("/desktop/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
router.StaticFile("/desktop/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
for i := 0; i < len(workboxFileNames); i++ {
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
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 {
devRoute := router.Group("/dev")
devRoute.GET("/cookies", bindMiddleware(middlewares.ServerSettingsCookie(config)))
}
proxyRoute := router.Group("/proxy")
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
if config.EnableMapDataFetchProxy {
if config.MapProvider == settings.OpenStreetMapProvider ||
config.MapProvider == settings.OpenStreetMapHumanitarianStyleProvider ||
config.MapProvider == settings.OpenTopoMapProvider ||
config.MapProvider == settings.OPNVKarteMapProvider ||
config.MapProvider == settings.CyclOSMMapProvider ||
config.MapProvider == settings.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))
}
}
}
if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
amapApiProxyRoute := router.Group("/_AMapService")
amapApiProxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByCookie))
{
amapApiProxyRoute.GET("/*action", bindProxy(api.AmapApis.AmapApiProxyHandler))
}
}
qrCodeRoute := router.Group("/qrcode")
qrCodeRoute.Use(bindMiddleware(middlewares.RequestId(config)))
{
qrCodeCacheStore := persistence.NewInMemoryStore(time.Minute)
qrCodeRoute.GET("/mobile_url.png", bindCachedImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore))
}
apiRoute := router.Group("/api")
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
apiRoute.Use(bindMiddleware(middlewares.RequestLog))
{
apiRoute.POST("/authorize.json", bindApi(api.Authorizations.AuthorizeHandler))
apiRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.AuthorizeHandler, config))
if config.EnableTwoFactor {
twoFactorRoute := apiRoute.Group("/2fa")
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization))
{
twoFactorRoute.POST("/authorize.json", bindApi(api.Authorizations.TwoFactorAuthorizeHandler))
twoFactorRoute.POST("/recovery.json", bindApi(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler))
twoFactorRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeHandler, config))
twoFactorRoute.POST("/recovery.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler, config))
}
}
if config.EnableUserRegister {
apiRoute.POST("/register.json", bindApi(api.Users.UserRegisterHandler))
apiRoute.POST("/register.json", bindApiWithTokenUpdate(api.Users.UserRegisterHandler, config))
}
if config.EnableDataExport {
dataRoute := apiRoute.Group("/data")
dataRoute.Use(bindMiddleware(middlewares.HeaderInQueryString))
dataRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
if config.EnableUserVerifyEmail {
apiRoute.POST("/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByUnloginUserHandler))
emailVerifyRoute := apiRoute.Group("/verify_email")
emailVerifyRoute.Use(bindMiddleware(middlewares.JWTEmailVerifyAuthorization))
{
dataRoute.GET("/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
emailVerifyRoute.POST("/by_token.json", bindApi(api.Users.UserEmailVerifyHandler))
}
}
apiRoute.GET("/logout.json", bindApi(api.Tokens.TokenRevokeCurrentHandler))
if config.EnableUserForgetPassword {
apiRoute.POST("/forget_password/request.json", bindApi(api.ForgetPasswords.UserForgetPasswordRequestHandler))
resetPasswordRoute := apiRoute.Group("/forget_password/reset")
resetPasswordRoute.Use(bindMiddleware(middlewares.JWTResetPasswordAuthorization))
{
resetPasswordRoute.POST("/by_token.json", bindApi(api.ForgetPasswords.UserResetPasswordHandler))
}
}
apiRoute.GET("/logout.json", bindApiWithTokenUpdate(api.Tokens.TokenRevokeCurrentHandler, config))
apiV1Route := apiRoute.Group("/v1")
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization))
@@ -169,24 +247,39 @@ func startWebServer(c *cli.Context) error {
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
apiV1Route.POST("/tokens/revoke_all.json", bindApi(api.Tokens.TokenRevokeAllHandler))
apiV1Route.POST("/tokens/refresh.json", bindApi(api.Tokens.TokenRefreshHandler))
apiV1Route.POST("/tokens/refresh.json", bindApiWithTokenUpdate(api.Tokens.TokenRefreshHandler, config))
// Users
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
apiV1Route.POST("/users/profile/update.json", bindApi(api.Users.UserUpdateProfileHandler))
apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config))
// Two Factor Authorization
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
if config.EnableTwoFactor {
apiV1Route.GET("/users/2fa/status.json", bindApi(api.TwoFactorAuthorizations.TwoFactorStatusHandler))
apiV1Route.POST("/users/2fa/enable/request.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableRequestHandler))
apiV1Route.POST("/users/2fa/enable/confirm.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableConfirmHandler))
apiV1Route.POST("/users/2fa/enable/confirm.json", bindApiWithTokenUpdate(api.TwoFactorAuthorizations.TwoFactorEnableConfirmHandler, config))
apiV1Route.POST("/users/2fa/disable.json", bindApi(api.TwoFactorAuthorizations.TwoFactorDisableHandler))
apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler))
}
// Data
apiV1Route.GET("/data/statistics.json", bindApi(api.DataManagements.DataStatisticsHandler))
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
if config.EnableDataExport {
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataToEzbookkeepingCSVHandler))
apiV1Route.GET("/data/export.tsv", bindTsv(api.DataManagements.ExportDataToEzbookkeepingTSVHandler))
}
// Accounts
apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler))
apiV1Route.GET("/accounts/get.json", bindApi(api.Accounts.AccountGetHandler))
@@ -201,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))
@@ -227,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))
}
@@ -235,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
}
@@ -274,6 +376,23 @@ func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
}
}
func bindApiWithTokenUpdate(fn core.ApiHandlerFunc, config *settings.Config) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, err := fn(c)
if err == nil && config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
middlewares.AmapApiProxyAuthCookie(c, config)
}
if err != nil {
utils.PrintJsonErrorResult(c, err)
} else {
utils.PrintJsonSuccessResult(c, result)
}
}
}
func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
@@ -286,3 +405,55 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
}
}
}
func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, fileName, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
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)
}
})
}
func bindProxy(fn core.ProxyHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
proxy, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
proxy.ServeHTTP(c.Writer, c.Request)
}
}
}
+208 -16
View File
@@ -25,16 +25,16 @@ 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)
# Static file root path (relative or absolute path)
static_root_path = public
# Enable GZip
enable_gzip = false
# Set to true to log each request and execution times
# Set to true to log each request and execution time
log_request = true
[database]
@@ -47,27 +47,40 @@ 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, absolute path of db file
# For "sqlite3" database only, database file path (relative or absolute path)
db_path = data/ezbookkeeping.db
# Max idle connection number, default is 2
# Max idle connection number (0 - 65535, 0 means no idle connections are retained), default is 2
max_idle_conn = 2
# Max opened connection number, default is 0 (unlimited)
# Max opened connection number (0 - 65535), default is 0 (unlimited)
max_open_conn = 0
# Max connection lifetime (seconds), default is 14400 (4 hours)
# Max connection lifetime (0 - 4294967295 seconds), default is 14400 (4 hours)
conn_max_lifetime = 14400
# Set to true to log each sql statement and execution times
# Set to true to log each sql statement and execution time
log_query = false
# Set to true to automatically update database structure when starting web server
auto_update_database = true
[mail]
# Set to true to enable sending mail by SMTP server
enable_smtp = false
# SMTP Server connection configuration
smtp_host = 127.0.0.1:25
smtp_user =
smtp_passwd =
smtp_skip_tls_verify = false
# Mail from address. This can be just an email address, or the "Name" <user@domain.com> format.
from_address =
[log]
# Either "console", "file", default is "console"
# Use space to separate multiple modes, e.g. "console file"
@@ -76,29 +89,90 @@ mode = console file
# Either "debug", "info", "warn", "error", default is "info"
level = info
# For "file" only, absolute path of log file
# 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
# 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, default is 2592000 (30 days)
# Token expired seconds (60 - 4294967295), default is 2592000 (30 days)
token_expired_time = 2592000
# Temporary token expired seconds, 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 (60 - 4294967295), default is 3600 (60 minutes)
email_verify_token_expired_time = 3600
# 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
request_id_header = true
@@ -106,13 +180,131 @@ request_id_header = true
# Set to true to allow users to register account by themselves
enable_register = true
# Set to true to allow users to verify email address
enable_email_verify = false
# Set to true to require email must be verified when login
enable_force_email_verify = false
# Set to true to allow users to reset password
enable_forget_password = true
# Set to true to require email must be verified when use forget password
forget_password_require_email_verify = false
# User avatar provider, supports the following types:
# "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 = 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
# "openstreetmap_humanitarian": http://map.hotosm.org
# "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 forward map data requests, for "openstreetmap", "openstreetmap_humanitarian", "opentopomap", "opnvkarte", "cyclosm", "cartodb", "tomtom", "tianditu" or "custom"
map_data_fetch_proxy = false
# 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 "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" 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" 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" 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" 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" 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 "euro_central_bank", "bank_of_canada", "reserve_bank_of_australia", "czech_national_bank", "national_bank_of_poland" currently
# Exchange rates data source, supports the following types:
# "euro_central_bank"
# "bank_of_canada"
# "reserve_bank_of_australia",
# "czech_national_bank"
# "national_bank_of_poland"
# "monetary_authority_of_singapore"
data_source = euro_central_bank
# Requesting exchange rates data timeout (milliseconds), default is 10000 (10 seconds)
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
# Set to 0 to disable timeout for requesting exchange rates data, default is 10000 (10 seconds)
request_timeout = 10000
# 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
+10
View File
@@ -9,6 +9,7 @@ import (
"github.com/urfave/cli/v2"
"github.com/mayswind/ezbookkeeping/cmd"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -24,6 +25,9 @@ var (
)
func main() {
settings.Version = Version
settings.CommitHash = CommitHash
app := &cli.App{
Name: "ezBookkeeping",
Usage: "A lightweight personal bookkeeping app hosted by yourself.",
@@ -32,12 +36,18 @@ func main() {
cmd.WebServer,
cmd.Database,
cmd.UserData,
cmd.SecurityUtils,
cmd.Utilities,
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "conf-path",
Usage: "Custom config `FILE` path",
},
&cli.BoolFlag{
Name: "no-boot-log",
Usage: "Disable boot log",
},
},
}
+73 -16
View File
@@ -1,21 +1,78 @@
module github.com/mayswind/ezbookkeeping
go 1.14
go 1.21
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/gzip v0.0.3
github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.4.1
github.com/go-sql-driver/mysql v1.5.0
github.com/lib/pq v1.8.0
github.com/mattn/go-sqlite3 v1.14.4
github.com/pquerna/otp v1.3.0
github.com/sirupsen/logrus v1.7.0
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.6.1
github.com/urfave/cli/v2 v2.3.0
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
gopkg.in/ini.v1 v1.62.0
xorm.io/xorm v1.0.5
github.com/boombuler/barcode v1.0.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.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.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.25.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/mail.v2 v2.3.1
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-20230905024940-24af94b03874 // indirect
github.com/buger/jsonparser v1.1.1 // 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/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.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/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.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.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.12 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // 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
)
+151 -110
View File
@@ -1,141 +1,182 @@
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=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/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/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
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.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/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/dustin/go-humanize 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/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
github.com/gabriel-vasile/mimetype v1.4.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.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
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.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.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.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
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/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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/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.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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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/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/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/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/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
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/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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
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.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/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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/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/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.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/sys v0.0.0-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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.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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.0.5 h1:LRr5PfOUb4ODPR63YwbowkNDwcolT2LnkwP/TUaMaB0=
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
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=
+7992 -12562
View File
File diff suppressed because it is too large Load Diff
+40 -44
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "0.1.0",
"version": "0.5.0",
"private": true,
"repository": {
"type": "git",
@@ -12,56 +12,52 @@
"url": "https://github.com/mayswind/ezbookkeeping/issues"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"serve": "cross-env NODE_ENV=development vite",
"build": "cross-env NODE_ENV=production vite build",
"serve:dist": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"axios": "^0.21.1",
"@mdi/js": "^7.4.47",
"@vuepic/vue-datepicker": "^8.8.1",
"axios": "^1.7.2",
"cbor-js": "^0.1.0",
"core-js": "^3.6.5",
"crypto-js": "^4.0.0",
"framework7": "^5.7.14",
"framework7-icons": "^3.0.1",
"framework7-vue": "^5.7.14",
"js-cookie": "^2.2.1",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dom7": "^4.0.6",
"echarts": "^5.5.1",
"framework7": "^8.3.3",
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.3.3",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"line-awesome": "^1.3.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"pinia": "^2.1.7",
"register-service-worker": "^1.7.2",
"ua-parser-js": "^0.7.28",
"vue": "^2.6.12",
"vue-clipboard2": "^0.3.1",
"vue-i18n": "^8.24.3",
"vue-pincode-input": "^0.4.0",
"vuex": "^3.6.2"
"skeleton-elements": "^4.0.1",
"swiper": "^10.2.0",
"ua-parser-js": "^1.0.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.6.11"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.11",
"@vue/cli-plugin-eslint": "^4.5.11",
"@vue/cli-plugin-pwa": "^4.5.11",
"@vue/cli-service": "^4.5.11",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"git-revision-webpack-plugin": "^3.0.6",
"moment-locales-webpack-plugin": "^1.2.0",
"vue-template-compiler": "^2.6.12"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
"@vitejs/plugin-vue": "^5.0.5",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0",
"git-rev-sync": "^3.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%",
+84 -27
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"
)
@@ -24,7 +27,7 @@ var (
)
// AccountListHandler returns accounts list of current user
func (a *AccountsApi) AccountListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountListHandler(c *core.Context) (any, *errs.Error) {
var accountListReq models.AccountListRequest
err := c.ShouldBindQuery(&accountListReq)
@@ -34,11 +37,11 @@ func (a *AccountsApi) AccountListHandler(c *core.Context) (interface{}, *errs.Er
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(uid)
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountListHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
userAllAccountResps := make([]*models.AccountInfoResponse, len(accounts))
@@ -84,7 +87,7 @@ func (a *AccountsApi) AccountListHandler(c *core.Context) (interface{}, *errs.Er
}
// AccountGetHandler returns one specific account of current user
func (a *AccountsApi) AccountGetHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountGetHandler(c *core.Context) (any, *errs.Error) {
var accountGetReq models.AccountGetRequest
err := c.ShouldBindQuery(&accountGetReq)
@@ -94,11 +97,11 @@ func (a *AccountsApi) AccountGetHandler(c *core.Context) (interface{}, *errs.Err
}
uid := c.GetCurrentUid()
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(uid, accountGetReq.Id)
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountGetHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountGetReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
accountRespMap := make(map[int64]*models.AccountInfoResponse)
@@ -127,7 +130,7 @@ func (a *AccountsApi) AccountGetHandler(c *core.Context) (interface{}, *errs.Err
}
// AccountCreateHandler saves a new account by request parameters for current user
func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
var accountCreateReq models.AccountCreateRequest
err := c.ShouldBindJSON(&accountCreateReq)
@@ -136,9 +139,21 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
if accountCreateReq.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
}
@@ -148,7 +163,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
}
} 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
}
@@ -166,17 +181,17 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
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
}
}
@@ -186,17 +201,53 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
}
uid := c.GetCurrentUid()
maxOrderId, err := a.accounts.GetMaxDisplayOrder(uid, accountCreateReq.Category)
maxOrderId, err := a.accounts.GetMaxDisplayOrder(c, uid, accountCreateReq.Category)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
err = a.accounts.CreateAccounts(mainAccount, childrenAccounts)
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 {
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to create account \"id:%d\" for user \"uid:%d\", because %s", mainAccount.AccountId, uid, err.Error())
@@ -205,6 +256,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
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 {
@@ -219,7 +271,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
}
// AccountModifyHandler saves an existed account by request parameters for current user
func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
var accountModifyReq models.AccountModifyRequest
err := c.ShouldBindJSON(&accountModifyReq)
@@ -228,12 +280,17 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.
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(uid, accountModifyReq.Id)
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountModifyHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
@@ -275,7 +332,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.
return nil, errs.ErrNothingWillBeUpdated
}
err = a.accounts.ModifyAccounts(uid, toUpdateAccounts)
err = a.accounts.ModifyAccounts(c, uid, toUpdateAccounts)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountModifyHandler] failed to update account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
@@ -325,7 +382,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.
}
// AccountHideHandler hides an existed account by request parameters for current user
func (a *AccountsApi) AccountHideHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountHideHandler(c *core.Context) (any, *errs.Error) {
var accountHideReq models.AccountHideRequest
err := c.ShouldBindJSON(&accountHideReq)
@@ -335,7 +392,7 @@ func (a *AccountsApi) AccountHideHandler(c *core.Context) (interface{}, *errs.Er
}
uid := c.GetCurrentUid()
err = a.accounts.HideAccount(uid, []int64{accountHideReq.Id}, accountHideReq.Hidden)
err = a.accounts.HideAccount(c, uid, []int64{accountHideReq.Id}, accountHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountHideHandler] failed to hide account \"id:%d\" for user \"uid:%d\", because %s", accountHideReq.Id, uid, err.Error())
@@ -347,7 +404,7 @@ func (a *AccountsApi) AccountHideHandler(c *core.Context) (interface{}, *errs.Er
}
// AccountMoveHandler moves display order of existed accounts by request parameters for current user
func (a *AccountsApi) AccountMoveHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountMoveHandler(c *core.Context) (any, *errs.Error) {
var accountMoveReq models.AccountMoveRequest
err := c.ShouldBindJSON(&accountMoveReq)
@@ -370,7 +427,7 @@ func (a *AccountsApi) AccountMoveHandler(c *core.Context) (interface{}, *errs.Er
accounts[i] = account
}
err = a.accounts.ModifyAccountDisplayOrders(uid, accounts)
err = a.accounts.ModifyAccountDisplayOrders(c, uid, accounts)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountMoveHandler] failed to move accounts for user \"uid:%d\", because %s", uid, err.Error())
@@ -382,7 +439,7 @@ func (a *AccountsApi) AccountMoveHandler(c *core.Context) (interface{}, *errs.Er
}
// AccountDeleteHandler deletes an existed account by request parameters for current user
func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (any, *errs.Error) {
var accountDeleteReq models.AccountDeleteRequest
err := c.ShouldBindJSON(&accountDeleteReq)
@@ -392,7 +449,7 @@ func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (interface{}, *errs.
}
uid := c.GetCurrentUid()
err = a.accounts.DeleteAccount(uid, accountDeleteReq.Id)
err = a.accounts.DeleteAccount(c, uid, accountDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountDeleteHandler] failed to delete account \"id:%d\" for user \"uid:%d\", because %s", accountDeleteReq.Id, uid, err.Error())
@@ -403,7 +460,7 @@ func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (interface{}, *errs.
return true, nil
}
func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.AccountCreateRequest, order int) *models.Account {
func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.AccountCreateRequest, order int32) *models.Account {
return &models.Account{
Uid: uid,
Name: accountCreateReq.Name,
@@ -425,7 +482,7 @@ func (a *AccountsApi) createSubAccountModels(uid int64, accountCreateReq *models
childrenAccounts := make([]*models.Account, len(accountCreateReq.SubAccounts))
for i := 0; i < len(accountCreateReq.SubAccounts); i++ {
for i := int32(0); i < int32(len(accountCreateReq.SubAccounts)); i++ {
childrenAccounts[i] = a.createNewAccountModel(uid, accountCreateReq.SubAccounts[i], i+1)
}
+61
View File
@@ -0,0 +1,61 @@
package api
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const amapCustomMapStylesUrl = "https://webapi.amap.com/v4/map/styles"
const amapOverseasMapUrl = "https://fmap01.amap.com/v3/vectormap"
const amapRestApiUrl = "https://restapi.amap.com/"
// AmapApiProxy represents amap api proxy
type AmapApiProxy struct {
}
// Initialize a amap api proxy singleton instance
var (
AmapApis = &AmapApiProxy{}
)
// AmapApiProxyHandler returns amap api response
func (p *AmapApiProxy) AmapApiProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
var targetUrl string
if strings.HasPrefix(c.Request.RequestURI, "/_AMapService/v4/map/styles") {
targetUrl = amapCustomMapStylesUrl + strings.TrimPrefix(c.Request.URL.Path, "/_AMapService/v4/map/styles")
} else if strings.HasPrefix(c.Request.RequestURI, "/_AMapService/v3/vectormap") {
targetUrl = amapOverseasMapUrl + strings.TrimPrefix(c.Request.URL.Path, "/_AMapService/v3/vectormap")
} else {
targetUrl = amapRestApiUrl + strings.TrimPrefix(c.Request.URL.Path, "/_AMapService/")
}
director := func(req *http.Request) {
targetRawUrl := fmt.Sprintf("%s?%s&jscode=%s", targetUrl, req.URL.RawQuery, settings.Container.Current.AmapApplicationSecret)
targetUrl, _ := url.Parse(targetRawUrl)
oldCookies := req.Cookies()
req.Header.Del("Cookie")
for i := 0; i < len(oldCookies); i++ {
if strings.HasPrefix(oldCookies[i].Name, "ebk_") {
continue
}
req.AddCookie(oldCookies[i])
}
req.URL = targetUrl
req.RequestURI = req.URL.RequestURI()
req.Host = targetUrl.Host
}
return &httputil.ReverseProxy{Director: director}, nil
}
+79 -30
View File
@@ -8,6 +8,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// AuthorizationsApi represents authorization api
@@ -27,7 +28,7 @@ var (
)
// AuthorizeHandler verifies and authorizes current login request
func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error) {
var credential models.UserLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -36,14 +37,35 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
return nil, errs.ErrLoginNameOrPasswordInvalid
}
user, err := a.users.GetUserByUsernameOrEmailAndPassword(credential.LoginName, credential.Password)
user, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
return nil, errs.ErrLoginNameOrPasswordWrong
}
err = a.users.UpdateUserLastLoginTime(user.Uid)
if user.Disabled {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user is disabled", credential.LoginName)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
hasValidEmailVerifyToken, err := a.tokens.ExistsValidTokenByType(c, user.Uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed check whether user \"uid:%d\" has valid verify email token, because %s", user.Uid, err.Error())
hasValidEmailVerifyToken = false
}
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user has not verified email", credential.LoginName)
return nil, errs.NewErrorWithContext(errs.ErrEmailIsNotVerified, map[string]any{
"email": user.Email,
"hasValidEmailVerifyToken": hasValidEmailVerifyToken,
})
}
err = a.users.UpdateUserLastLoginTime(c, user.Uid)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -52,10 +74,10 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
twoFactorEnable := a.tokens.CurrentConfig().EnableTwoFactor
if twoFactorEnable {
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(user.Uid)
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, user.Uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
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)
}
}
@@ -64,9 +86,9 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
var claims *core.UserTokenClaims
if twoFactorEnable {
token, claims, err = a.tokens.CreateRequire2FAToken(user, c)
token, claims, err = a.tokens.CreateRequire2FAToken(c, user)
} else {
token, claims, err = a.tokens.CreateToken(user, c)
token, claims, err = a.tokens.CreateToken(c, user)
}
if err != nil {
@@ -74,16 +96,20 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
return nil, errs.ErrTokenGenerating
}
if !twoFactorEnable {
c.SetTextualToken(token)
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
authResp := a.getAuthResponse(token, twoFactorEnable, user)
authResp := a.getAuthResponse(c, token, twoFactorEnable, user)
return authResp, nil
}
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *errs.Error) {
var credential models.TwoFactorLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -93,10 +119,10 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac
}
uid := c.GetCurrentUid()
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
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)
}
@@ -105,37 +131,48 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac
return nil, errs.ErrPasscodeInvalid
}
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
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
}
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (any, *errs.Error) {
var credential models.TwoFactorRecoveryCodeLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -145,10 +182,10 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
}
uid := c.GetCurrentUid()
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
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)
}
@@ -156,46 +193,58 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
return nil, errs.ErrTwoFactorIsNotEnabled
}
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
return nil, errs.ErrUserNotFound
}
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(uid, credential.RecoveryCode, user.Salt)
if user.Disabled {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
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)
}
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has authorized two factor via recovery code \"%s\", token will be expired at %d", user.Uid, credential.RecoveryCode, claims.ExpiresAt)
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()),
}
}
+153 -76
View File
@@ -19,30 +19,149 @@ 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) {
// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataToEzbookkeepingCSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
return a.getExportedFileContent(c, "csv")
}
// 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
func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
totalAccountCount, err := a.accounts.GetTotalAccountCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total account count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalTransactionCategoryCount, err := a.categories.GetTotalCategoryCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction category count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalTransactionTagCount, err := a.tags.GetTotalTagCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction tag count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalTransactionCount, err := a.transactions.GetTotalTransactionCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
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
}
// ClearDataHandler deletes all user data
func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error) {
var clearDataReq models.ClearDataRequest
err := c.ShouldBindJSON(&clearDataReq)
if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, errs.ErrUserNotFound
}
if !a.users.IsPasswordEqualsUserPassword(clearDataReq.Password, user) {
return nil, errs.ErrUserPasswordWrong
}
err = a.transactions.DeleteAllTransactions(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.categories.DeleteAllCategories(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.tags.DeleteAllTags(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.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) getExportedFileContent(c *core.Context, fileType string) ([]byte, string, *errs.Error) {
if !settings.Container.Current.EnableDataExport {
return nil, "", errs.ErrDataExportNotAllowed
}
@@ -57,7 +176,7 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -67,28 +186,28 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
return nil, "", errs.ErrUserNotFound
}
accounts, err := a.accounts.GetAllAccountsByUid(uid)
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
categories, err := a.categories.GetAllCategoriesByUid(uid, 0, -1)
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tags, err := a.tags.GetAllTagsByUid(uid)
tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(uid)
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())
@@ -99,80 +218,38 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
categoryMap := a.categories.GetCategoryMapByList(categories)
tagMap := a.tags.GetTagMapByList(tags)
allTransactions, err := a.transactions.GetAllTransactions(uid, pageCountForDataExport, true)
allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
result, err := a.exporter.ToExportedContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
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)
fileName := a.getFileName(user, timezone, fileType)
return result, fileName, nil
}
// ClearDataHandler deletes all user data
func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (interface{}, *errs.Error) {
var clearDataReq models.ClearDataRequest
err := c.ShouldBindJSON(&clearDataReq)
if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
if err != nil {
if !errs.IsCustomError(err) {
log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, errs.ErrUserNotFound
}
if !a.users.IsPasswordEqualsUserPassword(clearDataReq.Password, user) {
return nil, errs.ErrUserPasswordWrong
}
err = a.transactions.DeleteAllTransactions(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error())
return nil, errs.ErrOperationFailed
}
err = a.categories.DeleteAllCategories(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error())
return nil, errs.ErrOperationFailed
}
err = a.tags.DeleteAllTags(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.Error())
return nil, 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) 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)
}
+2 -2
View File
@@ -14,11 +14,11 @@ var (
)
// ApiNotFound returns api not found error
func (a *DefaultApi) ApiNotFound(c *core.Context) (interface{}, *errs.Error) {
func (a *DefaultApi) ApiNotFound(c *core.Context) (any, *errs.Error) {
return nil, errs.ErrApiNotFound
}
// MethodNotAllowed returns method not allowed error
func (a *DefaultApi) MethodNotAllowed(c *core.Context) (interface{}, *errs.Error) {
func (a *DefaultApi) MethodNotAllowed(c *core.Context) (any, *errs.Error) {
return nil, errs.ErrMethodNotAllowed
}
+21 -5
View File
@@ -1,7 +1,9 @@
package api
import (
"io/ioutil"
"crypto/tls"
"fmt"
"io"
"net/http"
"sort"
"time"
@@ -12,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
@@ -23,7 +26,7 @@ var (
)
// LatestExchangeRateHandler returns latest exchange rate data
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *errs.Error) {
dataSource := exchangerates.Container.Current
if dataSource == nil {
@@ -32,15 +35,28 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (interface
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{
InsecureSkipVerify: true,
}
}
client := &http.Client{
Timeout: time.Duration(settings.Container.Current.ExchangeRatesRequestTimeout) * time.Millisecond,
Transport: transport,
Timeout: time.Duration(settings.Container.Current.ExchangeRatesRequestTimeout) * time.Millisecond,
}
urls := dataSource.GetRequestUrls()
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())
@@ -53,7 +69,7 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (interface
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
exchangeRateResp, err := dataSource.Parse(c, body)
if err != nil {
+152
View File
@@ -0,0 +1,152 @@
package api
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// ForgetPasswordsApi represents user forget password api
type ForgetPasswordsApi struct {
users *services.UserService
tokens *services.TokenService
forgetPasswords *services.ForgetPasswordService
}
// Initialize a user api singleton instance
var (
ForgetPasswords = &ForgetPasswordsApi{
users: services.Users,
tokens: services.Tokens,
forgetPasswords: services.ForgetPasswords,
}
)
// UserForgetPasswordRequestHandler generates password reset link and send user an email with this link
func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (any, *errs.Error) {
var request models.ForgetPasswordRequest
err := c.ShouldBindJSON(&request)
if err != nil {
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] parse request failed, because %s", err.Error())
return nil, errs.ErrEmailIsEmptyOrInvalid
}
user, err := a.users.GetUserByEmail(c, request.Email)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
if !settings.Container.Current.EnableSMTP {
return nil, errs.ErrSMTPServerNotEnabled
}
token, _, err := a.tokens.CreatePasswordResetToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
go func() {
err = a.forgetPasswords.SendPasswordResetEmail(c, user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
}
}()
return true, nil
}
// UserResetPasswordHandler resets user password by request parameters
func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *errs.Error) {
var request models.PasswordResetRequest
err := c.ShouldBindJSON(&request)
if err != nil {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
if user.Email != request.Email {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
return nil, errs.ErrEmptyIsInvalid
}
if a.users.IsPasswordEqualsUserPassword(request.Password, user) {
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke password reset token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
return nil, errs.ErrNewPasswordEqualsOldInvalid
}
userNew := &models.User{
Uid: user.Uid,
Salt: user.Salt,
Password: request.Password,
}
_, _, err = a.users.UpdateUser(c, userNew, false)
if err != nil {
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
if err == nil {
log.InfofWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
} else {
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
return true, nil
}
+26
View File
@@ -0,0 +1,26 @@
package api
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// HealthsApi represents health api
type HealthsApi struct{}
// Initialize a healths api singleton instance
var (
Healths = &HealthsApi{}
)
// HealthStatusHandler returns the health status of current service
func (a *HealthsApi) HealthStatusHandler(c *core.Context) (any, *errs.Error) {
result := make(map[string]string)
result["version"] = settings.Version
result["commit"] = settings.CommitHash
result["status"] = "ok"
return result, nil
}
+127
View File
@@ -0,0 +1,127 @@
package api
import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
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 {
}
// Initialize a map image proxy singleton instance
var (
MapImages = &MapImageProxy{}
)
// MapTileImageProxyHandler returns map tile image
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
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.Container.Current.MapProvider {
return nil, errs.ErrMapProviderNotCurrent
}
zoomLevel := c.Param("zoomLevel")
coordinateX := c.Param("coordinateX")
fileName := c.Param("fileName")
fileNameParts := strings.Split(fileName, ".")
coordinateY := fileNameParts[0]
scale := c.Query("scale")
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
req.RequestURI = req.URL.RequestURI()
req.Host = imageUrl.Host
}
return &httputil.ReverseProxy{
Transport: transport,
Director: director,
}, nil
}
+51
View File
@@ -0,0 +1,51 @@
package api
import (
"bytes"
"image/png"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/qr"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const (
qrCodeDefaultWidth int = 320
qrCodeDefaultHeight int = 320
)
// QrCodesApi represents qrcode generator api
type QrCodesApi struct {
}
// Initialize a qrcode generator api singleton instance
var (
QrCodes = &QrCodesApi{}
)
// MobileUrlQrCodeHandler returns a mobile url qr code image
func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.Context) ([]byte, string, *errs.Error) {
fullUrl := settings.Container.Current.RootUrl + "mobile"
data, err := a.generateUrlQrCode(c, fullUrl)
if err != nil {
return nil, "", errs.ErrOperationFailed
}
return data, "image/png", nil
}
func (a *QrCodesApi) generateUrlQrCode(c *core.Context, url string) ([]byte, *errs.Error) {
qrCodeImg, _ := qr.Encode(url, qr.M, qr.Auto)
qrCodeImg, _ = barcode.Scale(qrCodeImg, qrCodeDefaultWidth, qrCodeDefaultHeight)
imgData := &bytes.Buffer{}
if err := png.Encode(imgData, qrCodeImg); err != nil {
return nil, errs.ErrOperationFailed
}
return imgData.Bytes(), nil
}
+63 -34
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"
)
@@ -26,13 +28,13 @@ var (
)
// TokenListHandler returns available token list of current user
func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TokensApi) TokenListHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(uid)
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tokenResps := make(models.TokenInfoResponseSlice, len(tokens))
@@ -44,11 +46,10 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error)
TokenId: a.tokens.GenerateTokenId(token),
TokenType: token.TokenType,
UserAgent: token.UserAgent,
CreatedAt: token.CreatedUnixTime,
ExpiredAt: token.ExpiredUnixTime,
LastSeen: token.LastSeenUnixTime,
}
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
tokenResp.IsCurrent = true
}
@@ -61,18 +62,11 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error)
}
// TokenRevokeCurrentHandler revokes current token of current user
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *errs.Error) {
_, claims, err := a.tokens.ParseToken(c)
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (any, *errs.Error) {
_, claims, err := a.tokens.ParseTokenByHeader(c)
if err != nil {
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid, err := utils.StringToInt64(claims.Id)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.TokenRevokeCurrentHandler] parse user id failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
@@ -83,25 +77,25 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *er
}
tokenRecord := &models.TokenRecord{
Uid: uid,
Uid: claims.Uid,
UserTokenId: userTokenId,
CreatedUnixTime: claims.IssuedAt,
}
tokenId := a.tokens.GenerateTokenId(tokenRecord)
err = a.tokens.DeleteToken(tokenRecord)
err = a.tokens.DeleteToken(c, tokenRecord)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
log.ErrorfWithRequestId(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenId)
log.InfofWithRequestId(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
return true, nil
}
// TokenRevokeHandler revokes specific token of current user
func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TokensApi) TokenRevokeHandler(c *core.Context) (any, *errs.Error) {
var tokenRevokeReq models.TokenRevokeRequest
err := c.ShouldBindJSON(&tokenRevokeReq)
@@ -127,7 +121,7 @@ func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Erro
return nil, errs.ErrInvalidTokenId
}
err = a.tokens.DeleteToken(tokenRecord)
err = a.tokens.DeleteToken(c, tokenRecord)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
@@ -139,13 +133,13 @@ func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Erro
}
// TokenRevokeAllHandler revokes all tokens of current user except current token
func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
tokens, err := a.tokens.GetAllTokensByUid(uid)
tokens, err := a.tokens.GetAllTokensByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
claims := c.GetTokenClaims()
@@ -154,7 +148,7 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
for i := 0; i < len(tokens); i++ {
token := tokens[i]
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
currentTokenIndex = i
break
}
@@ -162,7 +156,7 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
tokens = append(tokens[:currentTokenIndex], tokens[currentTokenIndex+1:]...)
err = a.tokens.DeleteTokens(uid, tokens)
err = a.tokens.DeleteTokens(c, uid, tokens)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
@@ -174,23 +168,56 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
}
// TokenRefreshHandler refresh current token of current user
func (a *TokensApi) TokenRefreshHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
return nil, errs.ErrUserNotFound
}
token, claims, err := a.tokens.CreateToken(user, c)
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 {
log.ErrorfWithRequestId(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrTokenGenerating)
}
oldTokenClaims := c.GetTokenClaims()
oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
oldTokenRecord := &models.TokenRecord{
Uid: uid,
@@ -198,14 +225,16 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (interface{}, *errs.Err
CreatedUnixTime: oldTokenClaims.IssuedAt,
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
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
+149 -77
View File
@@ -3,11 +3,16 @@ package api
import (
"sort"
"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
@@ -23,7 +28,7 @@ var (
)
// CategoryListHandler returns transaction category list of current user
func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (any, *errs.Error) {
var categoryListReq models.TransactionCategoryListRequest
err := c.ShouldBindQuery(&categoryListReq)
@@ -33,18 +38,18 @@ func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (interfa
}
uid := c.GetCurrentUid()
categories, err := a.categories.GetAllCategoriesByUid(uid, categoryListReq.Type, categoryListReq.ParentId)
categories, err := a.categories.GetAllCategoriesByUid(c, uid, categoryListReq.Type, categoryListReq.ParentId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryListHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return a.getTransactionCategoryListByTypeResponse(categories, categoryListReq.ParentId)
}
// CategoryGetHandler returns one specific transaction category of current user
func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (any, *errs.Error) {
var categoryGetReq models.TransactionCategoryGetRequest
err := c.ShouldBindQuery(&categoryGetReq)
@@ -54,11 +59,11 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (interfac
}
uid := c.GetCurrentUid()
category, err := a.categories.GetCategoryByCategoryId(uid, categoryGetReq.Id)
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryGetHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryGetReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
categoryResp := category.ToTransactionCategoryInfoResponse()
@@ -67,7 +72,7 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (interfac
}
// CategoryCreateHandler saves a new transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any, *errs.Error) {
var categoryCreateReq models.TransactionCategoryCreateRequest
err := c.ShouldBindJSON(&categoryCreateReq)
@@ -84,7 +89,7 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
uid := c.GetCurrentUid()
if categoryCreateReq.ParentId > 0 {
parentCategory, err := a.categories.GetCategoryByCategoryId(uid, categoryCreateReq.ParentId)
parentCategory, err := a.categories.GetCategoryByCategoryId(c, uid, categoryCreateReq.ParentId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get parent category \"id:%d\" for user \"uid:%d\", because %s", categoryCreateReq.ParentId, uid, err.Error())
@@ -102,22 +107,44 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
}
}
var maxOrderId int
var maxOrderId int32
if categoryCreateReq.ParentId <= 0 {
maxOrderId, err = a.categories.GetMaxDisplayOrder(uid, categoryCreateReq.Type)
maxOrderId, err = a.categories.GetMaxDisplayOrder(c, uid, categoryCreateReq.Type)
} else {
maxOrderId, err = a.categories.GetMaxSubCategoryDisplayOrder(uid, categoryCreateReq.Type, categoryCreateReq.ParentId)
maxOrderId, err = a.categories.GetMaxSubCategoryDisplayOrder(c, uid, categoryCreateReq.Type, categoryCreateReq.ParentId)
}
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
err = a.categories.CreateCategory(category)
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 {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to create category \"id:%d\" for user \"uid:%d\", because %s", category.CategoryId, uid, err.Error())
@@ -126,15 +153,16 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
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
}
// CategoryCreateBatchHandler saves some new transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (any, *errs.Error) {
var categoryCreateBatchReq models.TransactionCategoryCreateBatchRequest
err := c.ShouldBindJSON(&categoryCreateBatchReq)
err := c.ShouldBindBodyWith(&categoryCreateBatchReq, binding.JSON)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] parse request failed, because %s", err.Error())
@@ -143,58 +171,17 @@ func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (
uid := c.GetCurrentUid()
categoryTypeMaxOrderMap := make(map[models.TransactionCategoryType]int)
categoriesMap := make(map[*models.TransactionCategory][]*models.TransactionCategory)
categoriesMap[nil] = make([]*models.TransactionCategory, len(categoryCreateBatchReq.Categories))
totalCount := 0
for i := 0; i < len(categoryCreateBatchReq.Categories); i++ {
categoryCreateReq := categoryCreateBatchReq.Categories[i]
var maxOrderId, exists = categoryTypeMaxOrderMap[categoryCreateReq.Type]
if !exists {
maxOrderId, err = a.categories.GetMaxDisplayOrder(uid, categoryCreateReq.Type)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
}
category := a.createNewCategoryModel(uid, &models.TransactionCategoryCreateRequest{
Name: categoryCreateReq.Name,
Type: categoryCreateReq.Type,
Icon: categoryCreateReq.Icon,
Color: categoryCreateReq.Color,
}, maxOrderId+1)
categoriesMap[category] = make([]*models.TransactionCategory, len(categoryCreateReq.SubCategories))
for j := 0; j < len(categoryCreateReq.SubCategories); j++ {
subCategory := a.createNewCategoryModel(uid, categoryCreateReq.SubCategories[j], j+1)
categoriesMap[category][j] = subCategory
totalCount++
}
categoriesMap[nil][i] = category
categoryTypeMaxOrderMap[categoryCreateReq.Type] = maxOrderId + 1
totalCount++
}
categories, err := a.categories.CreateCategories(uid, categoriesMap)
categories, err := a.createBatchCategories(c, uid, &categoryCreateBatchReq)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] user \"uid:%d\" has created categoroies successfully", uid)
return a.getTransactionCategoryListByTypeResponse(categories, 0)
}
// CategoryModifyHandler saves an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any, *errs.Error) {
var categoryModifyReq models.TransactionCategoryModifyRequest
err := c.ShouldBindJSON(&categoryModifyReq)
@@ -204,24 +191,26 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
}
uid := c.GetCurrentUid()
category, err := a.categories.GetCategoryByCategoryId(uid, categoryModifyReq.Id)
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newCategory := &models.TransactionCategory{
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 &&
@@ -229,7 +218,39 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
return nil, errs.ErrNothingWillBeUpdated
}
err = a.categories.ModifyCategory(newCategory)
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 {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to update category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
@@ -239,7 +260,6 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
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()
@@ -247,7 +267,7 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
}
// CategoryHideHandler hides an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (any, *errs.Error) {
var categoryHideReq models.TransactionCategoryHideRequest
err := c.ShouldBindJSON(&categoryHideReq)
@@ -257,7 +277,7 @@ func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interfa
}
uid := c.GetCurrentUid()
err = a.categories.HideCategory(uid, []int64{categoryHideReq.Id}, categoryHideReq.Hidden)
err = a.categories.HideCategory(c, uid, []int64{categoryHideReq.Id}, categoryHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryHideHandler] failed to hide category \"id:%d\" for user \"uid:%d\", because %s", categoryHideReq.Id, uid, err.Error())
@@ -269,7 +289,7 @@ func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interfa
}
// CategoryMoveHandler moves display order of existed transaction categories by request parameters for current user
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (any, *errs.Error) {
var categoryMoveReq models.TransactionCategoryMoveRequest
err := c.ShouldBindJSON(&categoryMoveReq)
@@ -292,7 +312,7 @@ func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interfa
categories[i] = category
}
err = a.categories.ModifyCategoryDisplayOrders(uid, categories)
err = a.categories.ModifyCategoryDisplayOrders(c, uid, categories)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error())
@@ -304,7 +324,7 @@ func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interfa
}
// CategoryDeleteHandler deletes an existed transaction category by request parameters for current user
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (any, *errs.Error) {
var categoryDeleteReq models.TransactionCategoryDeleteRequest
err := c.ShouldBindJSON(&categoryDeleteReq)
@@ -314,7 +334,7 @@ func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (inter
}
uid := c.GetCurrentUid()
err = a.categories.DeleteCategory(uid, categoryDeleteReq.Id)
err = a.categories.DeleteCategory(c, uid, categoryDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] failed to delete category \"id:%d\" for user \"uid:%d\", because %s", categoryDeleteReq.Id, uid, err.Error())
@@ -325,7 +345,59 @@ func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (inter
return true, nil
}
func (a *TransactionCategoriesApi) createNewCategoryModel(uid int64, categoryCreateReq *models.TransactionCategoryCreateRequest, order int) *models.TransactionCategory {
func (a *TransactionCategoriesApi) createBatchCategories(c *core.Context, uid int64, categoryCreateBatchReq *models.TransactionCategoryCreateBatchRequest) ([]*models.TransactionCategory, error) {
var err error
categoryTypeMaxOrderMap := make(map[models.TransactionCategoryType]int32)
categoriesMap := make(map[*models.TransactionCategory][]*models.TransactionCategory)
categoriesMap[nil] = make([]*models.TransactionCategory, len(categoryCreateBatchReq.Categories))
totalCount := 0
for i := 0; i < len(categoryCreateBatchReq.Categories); i++ {
categoryCreateReq := categoryCreateBatchReq.Categories[i]
var maxOrderId, exists = categoryTypeMaxOrderMap[categoryCreateReq.Type]
if !exists {
maxOrderId, err = a.categories.GetMaxDisplayOrder(c, uid, categoryCreateReq.Type)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
category := a.createNewCategoryModel(uid, &models.TransactionCategoryCreateRequest{
Name: categoryCreateReq.Name,
Type: categoryCreateReq.Type,
Icon: categoryCreateReq.Icon,
Color: categoryCreateReq.Color,
}, maxOrderId+1)
categoriesMap[category] = make([]*models.TransactionCategory, len(categoryCreateReq.SubCategories))
for j := int32(0); j < int32(len(categoryCreateReq.SubCategories)); j++ {
subCategory := a.createNewCategoryModel(uid, categoryCreateReq.SubCategories[j], j+1)
categoriesMap[category][j] = subCategory
totalCount++
}
categoriesMap[nil][i] = category
categoryTypeMaxOrderMap[categoryCreateReq.Type] = maxOrderId + 1
totalCount++
}
categories, err := a.categories.CreateCategories(c, uid, categoriesMap)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.createBatchCategories] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_categories.createBatchCategories] user \"uid:%d\" has created categories successfully", uid)
return categories, nil
}
func (a *TransactionCategoriesApi) createNewCategoryModel(uid int64, categoryCreateReq *models.TransactionCategoryCreateRequest, order int32) *models.TransactionCategory {
return &models.TransactionCategory{
Uid: uid,
Name: categoryCreateReq.Name,
+27 -27
View File
@@ -23,13 +23,13 @@ var (
)
// TagListHandler returns transaction tag list of current user
func (a *TransactionTagsApi) TagListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagListHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
tags, err := a.tags.GetAllTagsByUid(uid)
tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagListHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tagResps := make(models.TransactionTagInfoResponseSlice, len(tags))
@@ -44,7 +44,7 @@ func (a *TransactionTagsApi) TagListHandler(c *core.Context) (interface{}, *errs
}
// TagGetHandler returns one specific transaction tag of current user
func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (any, *errs.Error) {
var tagGetReq models.TransactionTagGetRequest
err := c.ShouldBindQuery(&tagGetReq)
@@ -54,11 +54,11 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (interface{}, *errs.
}
uid := c.GetCurrentUid()
tag, err := a.tags.GetTagByTagId(uid, tagGetReq.Id)
tag, err := a.tags.GetTagByTagId(c, uid, tagGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagGetHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagGetReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tagResp := tag.ToTransactionTagInfoResponse()
@@ -67,7 +67,7 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (interface{}, *errs.
}
// TagCreateHandler saves a new transaction tag by request parameters for current user
func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (any, *errs.Error) {
var tagCreateReq models.TransactionTagCreateRequest
err := c.ShouldBindJSON(&tagCreateReq)
@@ -78,16 +78,16 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (interface{}, *er
uid := c.GetCurrentUid()
maxOrderId, err := a.tags.GetMaxDisplayOrder(uid)
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tag := a.createNewTagModel(uid, &tagCreateReq, maxOrderId+1)
err = a.tags.CreateTag(tag)
err = a.tags.CreateTag(c, tag)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to create tag \"id:%d\" for user \"uid:%d\", because %s", tag.TagId, uid, err.Error())
@@ -102,7 +102,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (interface{}, *er
}
// TagModifyHandler saves an existed transaction tag by request parameters for current user
func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (any, *errs.Error) {
var tagModifyReq models.TransactionTagModifyRequest
err := c.ShouldBindJSON(&tagModifyReq)
@@ -112,11 +112,11 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *er
}
uid := c.GetCurrentUid()
tag, err := a.tags.GetTagByTagId(uid, tagModifyReq.Id)
tag, err := a.tags.GetTagByTagId(c, uid, tagModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagModifyHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newTag := &models.TransactionTag{
@@ -129,7 +129,7 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *er
return nil, errs.ErrNothingWillBeUpdated
}
err = a.tags.ModifyTag(newTag)
err = a.tags.ModifyTag(c, newTag)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagModifyHandler] failed to update tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
@@ -145,34 +145,34 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *er
}
// TagHideHandler hides an transaction tag by request parameters for current user
func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error) {
var tagHideReq models.TransactionTagHideRequest
err := c.ShouldBindJSON(&tagHideReq)
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)
}
uid := c.GetCurrentUid()
err = a.tags.HideTag(uid, []int64{tagHideReq.Id}, tagHideReq.Hidden)
err = a.tags.HideTag(c, uid, []int64{tagHideReq.Id}, tagHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.CategoryHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
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
}
// TagMoveHandler moves display order of existed transaction tags by request parameters for current user
func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error) {
var tagMoveReq models.TransactionTagMoveRequest
err := c.ShouldBindJSON(&tagMoveReq)
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)
}
@@ -190,19 +190,19 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (interface{}, *errs
tags[i] = tag
}
err = a.tags.ModifyTagDisplayOrders(uid, tags)
err = a.tags.ModifyTagDisplayOrders(c, uid, tags)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.CategoryMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
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
}
// TagDeleteHandler deletes an existed transaction tag by request parameters for current user
func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (any, *errs.Error) {
var tagDeleteReq models.TransactionTagDeleteRequest
err := c.ShouldBindJSON(&tagDeleteReq)
@@ -212,7 +212,7 @@ func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (interface{}, *er
}
uid := c.GetCurrentUid()
err = a.tags.DeleteTag(uid, tagDeleteReq.Id)
err = a.tags.DeleteTag(c, uid, tagDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagDeleteHandler] failed to delete tag \"id:%d\" for user \"uid:%d\", because %s", tagDeleteReq.Id, uid, err.Error())
@@ -223,7 +223,7 @@ func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (interface{}, *er
return true, nil
}
func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.TransactionTagCreateRequest, order int) *models.TransactionTag {
func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.TransactionTagCreateRequest, order int32) *models.TransactionTag {
return &models.TransactionTag{
Uid: uid,
Name: tagCreateReq.Name,
+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,
}
}
+363 -186
View File
@@ -4,16 +4,18 @@ import (
"sort"
"strings"
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
@@ -35,7 +37,7 @@ var (
)
// TransactionCountHandler returns transaction total count of current user
func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (any, *errs.Error) {
var transactionCountReq models.TransactionCountRequest
err := c.ShouldBindQuery(&transactionCountReq)
@@ -46,14 +48,38 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{},
uid := c.GetCurrentUid()
allCategoryIds, err := a.getCategoryAndSubCategoryIds(transactionCountReq.CategoryId, 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.CategoryIds, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get transaction category error, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
totalCount, err := a.transactions.GetTransactionCount(uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, transactionCountReq.AccountId, transactionCountReq.Keyword)
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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
countResp := &models.TransactionCountResponse{
TotalCount: totalCount,
@@ -63,7 +89,7 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{},
}
// TransactionListHandler returns transaction list of current user
func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionListHandler(c *core.Context) (any, *errs.Error) {
var transactionListReq models.TransactionListByMaxTimeRequest
err := c.ShouldBindQuery(&transactionListReq)
@@ -80,7 +106,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -90,24 +116,54 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
return nil, errs.ErrUserNotFound
}
allCategoryIds, err := a.getCategoryAndSubCategoryIds(transactionListReq.CategoryId, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.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.CategoryIds, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get transaction category error, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions, err := a.transactions.GetTransactionsByMaxTime(uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, transactionListReq.AccountId, transactionListReq.Keyword, transactionListReq.Count+1, true)
var 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, 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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
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())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
hasMore := false
var nextTimeSequenceId *int64
if len(transactions) > transactionListReq.Count {
if len(transactions) > int(transactionListReq.Count) {
hasMore = true
nextTimeSequenceId = &transactions[transactionListReq.Count].TransactionTime
transactions = transactions[:transactionListReq.Count]
@@ -128,11 +184,15 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
transactionResps.NextTimeSequenceId = nextTimeSequenceId
}
if transactionListReq.WithCount {
transactionResps.TotalCount = &totalCount
}
return transactionResps, nil
}
// TransactionMonthListHandler returns transaction list of current user by month
func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interface{}, *errs.Error) {
// TransactionMonthListHandler returns all transaction list of current user by month
func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (any, *errs.Error) {
var transactionListReq models.TransactionListInMonthByPageRequest
err := c.ShouldBindQuery(&transactionListReq)
@@ -149,7 +209,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -159,25 +219,37 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
return nil, errs.ErrUserNotFound
}
allCategoryIds, err := a.getCategoryAndSubCategoryIds(transactionListReq.CategoryId, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.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.CategoryIds, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get transaction category error, because %s", err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, transactionListReq.AccountId, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, utcOffset)
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())
return nil, errs.ErrOperationFailed
}
totalCount, err := a.transactions.GetMonthTransactionCount(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, transactionListReq.AccountId, transactionListReq.Keyword, utcOffset)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthListHandler] failed to get transaction count in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionListResult(c, user, transactions, utcOffset, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
@@ -189,14 +261,14 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
transactionResps := &models.TransactionInfoPageWrapperResponse2{
Items: transactionResult,
TotalCount: totalCount,
TotalCount: int64(transactionResult.Len()),
}
return transactionResps, nil
}
// TransactionStatisticsHandler returns transaction statistics of current user
func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (any, *errs.Error) {
var statisticReq models.TransactionStatisticRequest
err := c.ShouldBindQuery(&statisticReq)
@@ -205,8 +277,20 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interfa
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(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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticResp := &models.TransactionStatisticResponse{
StartTime: statisticReq.StartTime,
@@ -227,8 +311,66 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interfa
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) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs.Error) {
var transactionAmountsReq models.TransactionAmountsRequest
err := c.ShouldBindQuery(&transactionAmountsReq)
@@ -249,31 +391,38 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
return nil, errs.ErrQueryItemsEmpty
}
if len(requestItems) > 5 {
if len(requestItems) > 20 {
log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are too many items")
return nil, errs.ErrQueryItemsTooMuch
}
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(uid)
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
accountMap := a.accounts.GetAccountMapByList(accounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsResp := make(map[string]*models.TransactionAmountsResponseItem)
amountsResp := orderedmap.New[string, *models.TransactionAmountsResponseItem]()
for i := 0; i < len(requestItems); i++ {
requestItem := requestItems[i]
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(uid, requestItem.StartTime, requestItem.EndTime)
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, 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())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsMap := make(map[string]*models.TransactionAmountsResponseItemAmountInfo)
@@ -322,130 +471,26 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
amountsMap[account.Currency] = totalAmounts
}
allTotalAmounts := make([]*models.TransactionAmountsResponseItemAmountInfo, 0)
allTotalAmounts := make(models.TransactionAmountsResponseItemAmountInfoSlice, 0)
for _, totalAmounts := range amountsMap {
allTotalAmounts = append(allTotalAmounts, totalAmounts)
}
amountsResp[requestItem.Name] = &models.TransactionAmountsResponseItem{
sort.Sort(allTotalAmounts)
amountsResp.Set(requestItem.Name, &models.TransactionAmountsResponseItem{
StartTime: requestItem.StartTime,
EndTime: requestItem.EndTime,
Amounts: allTotalAmounts,
}
}
return amountsResp, nil
}
// TransactionMonthAmountsHandler returns every month transaction amounts of current user
func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (interface{}, *errs.Error) {
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(uid)
accountMap := a.accounts.GetAccountMapByList(accounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalAmounts, err := a.transactions.GetAccountsMonthTotalIncomeAndExpense(uid, startTime, endTime, pageCountForLoadTransactionAmounts)
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.TransactionAmountsResponseItemAmountInfo, 0, len(monthTotalAmounts))
for _, monthTotalAmount := range monthTotalAmounts {
amounts = append(amounts, monthTotalAmount)
}
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) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (any, *errs.Error) {
var transactionGetReq models.TransactionGetRequest
err := c.ShouldBindQuery(&transactionGetReq)
@@ -462,7 +507,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -472,15 +517,15 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionGetReq.Id)
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionGetReq.Id, uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
transaction = a.transactions.GetRelatedTransferTransaction(transaction, transaction.RelatedId)
transaction = a.transactions.GetRelatedTransferTransaction(transaction)
}
accountIds := make([]int64, 0, 2)
@@ -491,7 +536,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
accountIds = utils.ToUniqueInt64Slice(accountIds)
}
accountMap, err := a.accounts.GetAccountsByAccountIds(uid, accountIds)
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, accountIds)
if _, exists := accountMap[transaction.AccountId]; !exists {
log.WarnfWithRequestId(c, "[transactions.TransactionGetHandler] account of transaction \"id:%d\" does not exist for user \"uid:%d\"", transaction.TransactionId, uid)
@@ -505,31 +550,31 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
}
}
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, []int64{transaction.TransactionId})
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, []int64{transaction.TransactionId})
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var category *models.TransactionCategory
var tagMap map[int64]*models.TransactionTag
if !transactionGetReq.TrimCategory {
category, err = a.transactionCategories.GetCategoryByCategoryId(uid, transaction.CategoryId)
category, err = a.transactionCategories.GetCategoryByCategoryId(c, uid, transaction.CategoryId)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions category for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
if !transactionGetReq.TrimTag {
tagMap, err = a.transactionTags.GetTagsByTagIds(uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds)))
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds)))
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
@@ -561,7 +606,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
}
// TransactionCreateHandler saves a new transaction by request parameters for current user
func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs.Error) {
var transactionCreateReq models.TransactionCreateRequest
err := c.ShouldBindJSON(&transactionCreateReq)
@@ -601,7 +646,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -611,14 +656,36 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
return nil, errs.ErrUserNotFound
}
transaction := a.createNewTransactionModel(uid, &transactionCreateReq)
transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
}
err = a.transactions.CreateTransaction(transaction, tagIds)
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 {
log.ErrorfWithRequestId(c, "[transactions.TransactionCreateHandler] failed to create transaction \"id:%d\" for user \"uid:%d\", because %s", transaction.TransactionId, uid, err.Error())
@@ -627,13 +694,14 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
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
}
// TransactionModifyHandler saves an existed transaction by request parameters for current user
func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (any, *errs.Error) {
var transactionModifyReq models.TransactionModifyRequest
err := c.ShouldBindJSON(&transactionModifyReq)
@@ -650,7 +718,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -660,7 +728,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionModifyReq.Id)
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error())
@@ -672,11 +740,11 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
return nil, errs.ErrTransactionTypeInvalid
}
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, []int64{transaction.TransactionId})
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, []int64{transaction.TransactionId})
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
@@ -702,6 +770,11 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
newTransaction.RelatedAccountAmount = transactionModifyReq.DestinationAmount
}
if transactionModifyReq.GeoLocation != nil {
newTransaction.GeoLongitude = transactionModifyReq.GeoLocation.Longitude
newTransaction.GeoLatitude = transactionModifyReq.GeoLocation.Latitude
}
if newTransaction.CategoryId == transaction.CategoryId &&
utils.GetUnixTimeFromTransactionTime(newTransaction.TransactionTime) == utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) &&
newTransaction.TimezoneUtcOffset == transaction.TimezoneUtcOffset &&
@@ -711,6 +784,8 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
(transaction.Type != models.TRANSACTION_DB_TYPE_TRANSFER_OUT || newTransaction.RelatedAccountAmount == transaction.RelatedAccountAmount) &&
newTransaction.HideAmount == transaction.HideAmount &&
newTransaction.Comment == transaction.Comment &&
newTransaction.GeoLongitude == transaction.GeoLongitude &&
newTransaction.GeoLatitude == transaction.GeoLatitude &&
utils.Int64SliceEquals(tagIds, transactionTagIds) {
return nil, errs.ErrNothingWillBeUpdated
}
@@ -730,7 +805,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
}
err = a.transactions.ModifyTransaction(newTransaction, addTransactionTagIds, removeTransactionTagIds)
err = a.transactions.ModifyTransaction(c, newTransaction, 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())
@@ -746,7 +821,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
}
// TransactionDeleteHandler deletes an existed transaction by request parameters for current user
func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (any, *errs.Error) {
var transactionDeleteReq models.TransactionDeleteRequest
err := c.ShouldBindJSON(&transactionDeleteReq)
@@ -763,7 +838,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -773,7 +848,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionDeleteReq.Id)
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionDeleteHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionDeleteReq.Id, uid, err.Error())
@@ -791,7 +866,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}
return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime
}
err = a.transactions.DeleteTransaction(uid, transactionDeleteReq.Id)
err = a.transactions.DeleteTransaction(c, uid, transactionDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionDeleteHandler] failed to delete transaction \"id:%d\" for user \"uid:%d\", because %s", transactionDeleteReq.Id, uid, err.Error())
@@ -826,28 +901,124 @@ func (a *TransactionsApi) filterTransactions(c *core.Context, uid int64, transac
return finalTransactions
}
func (a *TransactionsApi) getCategoryAndSubCategoryIds(categoryId int64, uid int64) ([]int64, error) {
var allCategoryIds []int64
func (a *TransactionsApi) getAccountOrSubAccountIds(c *core.Context, accountIds string, uid int64) ([]int64, error) {
if accountIds == "" || accountIds == "0" {
return nil, nil
}
if categoryId > 0 {
allSubCategories, err := a.transactionCategories.GetAllCategoriesByUid(uid, 0, categoryId)
requestAccountIds, err := utils.StringArrayToInt64Array(strings.Split(accountIds, ","))
if err != nil {
return nil, errs.Or(err, errs.ErrAccountIdInvalid)
}
var allAccountIds []int64
if len(requestAccountIds) > 0 {
allSubAccounts, err := a.accounts.GetSubAccountsByAccountIds(c, uid, requestAccountIds)
if err != nil {
return nil, err
}
if len(allSubCategories) > 0 {
for i := 0; i < len(allSubCategories); i++ {
allCategoryIds = append(allCategoryIds, allSubCategories[i].CategoryId)
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)
}
}
}
return allAccountIds, nil
}
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 len(requestCategoryIds) > 0 {
allSubCategories, err := a.transactionCategories.GetSubCategoriesByCategoryIds(c, uid, requestCategoryIds)
if err != nil {
return nil, err
}
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))
@@ -897,7 +1068,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
categoryIds = append(categoryIds, transactions[i].CategoryId)
}
allAccounts, err := a.accounts.GetAccountsByAccountIds(uid, utils.ToUniqueInt64Slice(accountIds))
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds))
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
@@ -906,7 +1077,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
transactions = a.filterTransactions(c, uid, transactions, allAccounts)
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, transactionIds)
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
@@ -917,7 +1088,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
var tagMap map[int64]*models.TransactionTag
if !trimCategory {
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(uid, utils.ToUniqueInt64Slice(categoryIds))
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds))
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error())
@@ -926,7 +1097,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
}
if !trimTag {
tagMap, err = a.transactionTags.GetTagsByTagIds(uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds)))
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds)))
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
@@ -940,7 +1111,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
transaction = a.transactions.GetRelatedTransferTransaction(transaction, transaction.RelatedId)
transaction = a.transactions.GetRelatedTransferTransaction(transaction)
}
transactionEditable := transaction.IsEditable(user, utcOffset, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId])
@@ -973,7 +1144,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
return result, nil
}
func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreateReq *models.TransactionCreateRequest) *models.Transaction {
func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreateReq *models.TransactionCreateRequest, clientIp string) *models.Transaction {
var transactionDbType models.TransactionDbType
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE {
@@ -996,6 +1167,7 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
Amount: transactionCreateReq.SourceAmount,
HideAmount: transactionCreateReq.HideAmount,
Comment: transactionCreateReq.Comment,
CreatedIp: clientIp,
}
if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER {
@@ -1003,5 +1175,10 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
transaction.RelatedAccountAmount = transactionCreateReq.DestinationAmount
}
if transactionCreateReq.GeoLocation != nil {
transaction.GeoLongitude = transactionCreateReq.GeoLocation.Longitude
transaction.GeoLatitude = transactionCreateReq.GeoLocation.Latitude
}
return transaction
}
+40 -39
View File
@@ -32,9 +32,9 @@ var (
)
// TwoFactorStatusHandler returns 2fa status of current user
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
if err == errs.ErrTwoFactorIsNotEnabled {
statusResp := &models.TwoFactorStatusResponse{
@@ -45,7 +45,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (in
}
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)
}
@@ -58,12 +58,12 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (in
}
// TwoFactorEnableRequestHandler returns a new 2fa secret and qr code for current user to set 2fa and verify passcode next
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -71,7 +71,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
return nil, errs.ErrTwoFactorAlreadyEnabled
}
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -81,17 +81,17 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
return nil, errs.ErrUserNotFound
}
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(user)
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor secret, because %s", err.Error())
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
}
@@ -110,7 +110,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
}
// TwoFactorEnableConfirmHandler enables 2fa for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Context) (any, *errs.Error) {
var confirmReq models.TwoFactorEnableConfirmRequest
err := c.ShouldBindJSON(&confirmReq)
@@ -120,10 +120,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
}
uid := c.GetCurrentUid()
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -131,7 +131,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
return nil, errs.ErrTwoFactorAlreadyEnabled
}
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -154,28 +154,28 @@ 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(uid, recoveryCodes, user.Salt)
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(twoFactorSetting)
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(c, twoFactorSetting)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor setting for user \"uid:%d\", because %s", uid, err.Error())
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(uid, now)
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
if err == nil {
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
@@ -183,7 +183,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -195,6 +195,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
return confirmResp, nil
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
@@ -208,7 +209,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
}
// TwoFactorDisableHandler disables 2fa for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (any, *errs.Error) {
var disableReq models.TwoFactorDisableRequest
err := c.ShouldBindJSON(&disableReq)
@@ -218,7 +219,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -232,10 +233,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
return nil, errs.ErrUserPasswordWrong
}
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -243,27 +244,27 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
return nil, errs.ErrTwoFactorIsNotEnabled
}
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor recovery codes for user \"uid:%d\"", uid)
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor recovery codes for user \"uid:%d\"", uid)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(uid)
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor setting for user \"uid:%d\"", uid)
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
}
// TwoFactorRecoveryCodeRegenerateHandler returns new 2fa recovery codes and revokes old recovery codes for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (any, *errs.Error) {
var regenerateReq models.TwoFactorRegenerateRecoveryCodeRequest
err := c.ShouldBindJSON(&regenerateReq)
@@ -273,7 +274,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -287,10 +288,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
return nil, errs.ErrUserPasswordWrong
}
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two-factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -301,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(uid, recoveryCodes, user.Salt)
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
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)
}
@@ -316,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
}
+577 -23
View File
@@ -1,40 +1,49 @@
package api
import (
"io"
"os"
"strings"
"time"
"github.com/gin-gonic/gin/binding"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/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"
)
// UsersApi represents user api
type UsersApi struct {
users *services.UserService
tokens *services.TokenService
users *services.UserService
tokens *services.TokenService
accounts *services.AccountService
}
// Initialize a user api singleton instance
var (
Users = &UsersApi{
users: services.Users,
tokens: services.Tokens,
users: services.Users,
tokens: services.Tokens,
accounts: services.Accounts,
}
)
// UserRegisterHandler saves a new user by request parameters
func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
if !settings.Container.Current.EnableUserRegister {
return nil, errs.ErrUserRegistrationNotAllowed
}
var userRegisterReq models.UserRegisterRequest
err := c.ShouldBindJSON(&userRegisterReq)
err := c.ShouldBindBodyWith(&userRegisterReq, binding.JSON)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
@@ -55,12 +64,13 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
Email: userRegisterReq.Email,
Nickname: userRegisterReq.Nickname,
Password: userRegisterReq.Password,
Language: userRegisterReq.Language,
DefaultCurrency: userRegisterReq.DefaultCurrency,
FirstDayOfWeek: userRegisterReq.FirstDayOfWeek,
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
}
err = a.users.CreateUser(user)
err = a.users.CreateUser(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
@@ -69,12 +79,47 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
authResp := &models.AuthResponse{
Need2FA: false,
User: user.ToUserBasicInfo(),
presetCategoriesSaved := false
if len(userRegisterReq.Categories) > 0 {
_, err = TransactionCategories.createBatchCategories(c, user.Uid, &userRegisterReq.TransactionCategoryCreateBatchRequest)
if err == nil {
presetCategoriesSaved = true
}
}
token, claims, err := a.tokens.CreateToken(user, c)
authResp := &models.RegisterResponse{
AuthResponse: models.AuthResponse{
Need2FA: false,
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterRegisterNotificationContent(user.Language, c.GetClientLocale()),
},
NeedVerifyEmail: settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableUserForceVerifyEmail,
PresetCategoriesSaved: presetCategoriesSaved,
}
if settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP {
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
} else {
go func() {
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
}
}()
}
}
if settings.Container.Current.EnableUserForceVerifyEmail {
return authResp, nil
}
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -82,6 +127,7 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
}
authResp.Token = token
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
@@ -89,10 +135,74 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
return authResp, nil
}
// UserProfileHandler returns user profile of current user
func (a *UsersApi) UserProfileHandler(c *core.Context) (interface{}, *errs.Error) {
// UserEmailVerifyHandler sets user email address verified
func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
var userVerifyEmailReq models.UserVerifyEmailRequest
err := c.ShouldBindJSON(&userVerifyEmailReq)
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if user.EmailVerified {
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" email has been verified", user.Uid)
return nil, errs.ErrEmailIsVerified
}
err = a.users.SetUserEmailVerified(c, user.Username)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to update user \"uid:%d\" email address verified, because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
if err == nil {
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] revoke old email verify tokens for user \"uid:%d\"", user.Uid)
} else {
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
resp := &models.UserVerifyEmailResponse{}
if userVerifyEmailReq.RequestNewToken {
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return resp, nil
}
resp.NewToken = token
resp.User = user.ToUserBasicInfo()
resp.NotificationContent = settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale())
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
}
return resp, nil
}
// UserProfileHandler returns user profile of current user
func (a *UsersApi) UserProfileHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -107,7 +217,7 @@ func (a *UsersApi) UserProfileHandler(c *core.Context) (interface{}, *errs.Error
}
// UserUpdateProfileHandler saves user profile by request parameters for current user
func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs.Error) {
func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error) {
var userUpdateReq models.UserProfileUpdateRequest
err := c.ShouldBindJSON(&userUpdateReq)
@@ -117,7 +227,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
@@ -159,6 +269,45 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
anythingUpdate = true
}
if userUpdateReq.DefaultAccountId > 0 && userUpdateReq.DefaultAccountId != user.DefaultAccountId {
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
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
}
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
anythingUpdate = true
} else {
userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID
}
modifyUserLanguage := false
if userUpdateReq.Language != user.Language {
user.Language = userUpdateReq.Language
userNew.Language = userUpdateReq.Language
modifyUserLanguage = true
anythingUpdate = true
}
if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency {
user.DefaultCurrency = userUpdateReq.DefaultCurrency
userNew.DefaultCurrency = userUpdateReq.DefaultCurrency
@@ -173,34 +322,159 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
userNew.FirstDayOfWeek = models.WEEKDAY_INVALID
}
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat {
user.LongDateFormat = *userUpdateReq.LongDateFormat
userNew.LongDateFormat = *userUpdateReq.LongDateFormat
anythingUpdate = true
} else {
userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID
userNew.LongDateFormat = models.LONG_DATE_FORMAT_INVALID
}
if userUpdateReq.ShortDateFormat != nil && *userUpdateReq.ShortDateFormat != user.ShortDateFormat {
user.ShortDateFormat = *userUpdateReq.ShortDateFormat
userNew.ShortDateFormat = *userUpdateReq.ShortDateFormat
anythingUpdate = true
} else {
userNew.ShortDateFormat = models.SHORT_DATE_FORMAT_INVALID
}
if userUpdateReq.LongTimeFormat != nil && *userUpdateReq.LongTimeFormat != user.LongTimeFormat {
user.LongTimeFormat = *userUpdateReq.LongTimeFormat
userNew.LongTimeFormat = *userUpdateReq.LongTimeFormat
anythingUpdate = true
} else {
userNew.LongTimeFormat = models.LONG_TIME_FORMAT_INVALID
}
if userUpdateReq.ShortTimeFormat != nil && *userUpdateReq.ShortTimeFormat != user.ShortTimeFormat {
user.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
userNew.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
anythingUpdate = true
} else {
userNew.ShortTimeFormat = models.SHORT_TIME_FORMAT_INVALID
}
if 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
}
keyProfileUpdated, err := a.users.UpdateUser(userNew)
keyProfileUpdated, emailSetToUnverified, err := a.users.UpdateUser(c, userNew, modifyUserLanguage)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if emailSetToUnverified {
user.EmailVerified = false
}
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
resp := &models.UserProfileUpdateResponse{
User: user.ToUserBasicInfo(),
}
if emailSetToUnverified && settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP {
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
} else {
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
} else {
go func() {
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
}
}()
}
}
}
if keyProfileUpdated {
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(uid, now)
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
if err == nil {
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
@@ -208,7 +482,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -216,6 +490,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
}
resp.NewToken = token
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
@@ -225,3 +500,282 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
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 {
return nil, errs.ErrEmailValidationNotAllowed
}
var userResendVerifyEmailReq models.UserResendVerifyEmailRequest
err := c.ShouldBindJSON(&userResendVerifyEmailReq)
user, err := a.users.GetUserByEmail(c, userResendVerifyEmailReq.Email)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if !a.users.IsPasswordEqualsUserPassword(userResendVerifyEmailReq.Password, user) {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] request password not equals to the user password")
return nil, errs.ErrUserPasswordWrong
}
if user.Disabled {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if user.EmailVerified {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" email has been verified", user.Uid)
return nil, errs.ErrEmailIsVerified
}
if !settings.Container.Current.EnableSMTP {
return nil, errs.ErrSMTPServerNotEnabled
}
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
go func() {
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
}
}()
return true, nil
}
// UserSendVerifyEmailByLoginedUserHandler sends logined user verify email
func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any, *errs.Error) {
if !settings.Container.Current.EnableUserVerifyEmail {
return nil, errs.ErrEmailValidationNotAllowed
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.EmailVerified {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] user \"uid:%d\" email has been verified", user.Uid)
return nil, errs.ErrEmailIsVerified
}
if !settings.Container.Current.EnableSMTP {
return nil, errs.ErrSMTPServerNotEnabled
}
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
go func() {
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
if err != nil {
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
}
}()
return true, nil
}
// 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
}
+276 -42
View File
@@ -10,6 +10,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
@@ -19,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
@@ -26,12 +28,14 @@ type UserDataCli struct {
users *services.UserService
twoFactorAuthorizations *services.TwoFactorAuthorizationService
tokens *services.TokenService
forgetPasswords *services.ForgetPasswordService
}
// Initialize an user data cli singleton instance
var (
UserData = &UserDataCli{
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
@@ -39,6 +43,7 @@ var (
users: services.Users,
twoFactorAuthorizations: services.TwoFactorAuthorizations,
tokens: services.Tokens,
forgetPasswords: services.ForgetPasswords,
}
)
@@ -84,7 +89,7 @@ func (l *UserDataCli) AddNewUser(c *cli.Context, username string, email string,
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
}
err := l.users.CreateUser(user)
err := l.users.CreateUser(nil, user)
if err != nil {
log.BootErrorf("[user_data.AddNewUser] failed to create user \"%s\", because %s", user.Username, err.Error())
@@ -103,7 +108,7 @@ func (l *UserDataCli) GetUserByUsername(c *cli.Context, username string) (*model
return nil, errs.ErrUsernameIsEmpty
}
user, err := l.users.GetUserByUsername(username)
user, err := l.users.GetUserByUsername(nil, username)
if err != nil {
log.BootErrorf("[user_data.GetUserByUsername] failed to get user by user name \"%s\", because %s", username, err.Error())
@@ -125,7 +130,7 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
return errs.ErrPasswordIsEmpty
}
user, err := l.users.GetUserByUsername(username)
user, err := l.users.GetUserByUsername(nil, username)
if err != nil {
log.BootErrorf("[user_data.ModifyUserPassword] failed to get user by user name \"%s\", because %s", username, err.Error())
@@ -142,7 +147,7 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
Password: password,
}
_, err = l.users.UpdateUser(userNew)
_, _, err = l.users.UpdateUser(nil, userNew, false)
if err != nil {
log.BootErrorf("[user_data.ModifyUserPassword] failed to update user \"%s\" password, because %s", user.Username, err.Error())
@@ -150,7 +155,7 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
}
now := time.Now().Unix()
err = l.tokens.DeleteTokensBeforeTime(user.Uid, now)
err = l.tokens.DeleteTokensBeforeTime(nil, user.Uid, now)
if err == nil {
log.BootInfof("[user_data.ModifyUserPassword] revoke old tokens before unix time \"%d\" for user \"%s\"", now, user.Username)
@@ -161,6 +166,150 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
return nil
}
// SendPasswordResetMail sends an email with password reset link
func (l *UserDataCli) SendPasswordResetMail(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.SendPasswordResetMail] user name is empty")
return errs.ErrUsernameIsEmpty
}
user, err := l.users.GetUserByUsername(nil, username)
if err != nil {
log.BootErrorf("[user_data.SendPasswordResetMail] failed to get user by user name \"%s\", because %s", username, err.Error())
return err
}
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
log.BootWarnf("[user_data.SendPasswordResetMail] user \"uid:%d\" has not verified email", user.Uid)
return errs.ErrEmailIsNotVerified
}
token, _, err := l.tokens.CreatePasswordResetToken(nil, user)
if err != nil {
log.BootErrorf("[user_data.SendPasswordResetMail] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return err
}
err = l.forgetPasswords.SendPasswordResetEmail(nil, user, token, "")
if err != nil {
log.BootWarnf("[user_data.SendPasswordResetMail] cannot send email to \"%s\", because %s", user.Email, err.Error())
return err
}
return nil
}
// EnableUser sets user enabled according to the specified user name
func (l *UserDataCli) EnableUser(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.EnableUser] user name is empty")
return errs.ErrUsernameIsEmpty
}
err := l.users.EnableUser(nil, username)
if err != nil {
log.BootErrorf("[user_data.EnableUser] failed to set user enabled by user name \"%s\", because %s", username, err.Error())
return err
}
return nil
}
// DisableUser sets user disabled according to the specified user name
func (l *UserDataCli) DisableUser(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.DisableUser] user name is empty")
return errs.ErrUsernameIsEmpty
}
err := l.users.DisableUser(nil, username)
if err != nil {
log.BootErrorf("[user_data.DisableUser] failed to set user disabled by user name \"%s\", because %s", username, err.Error())
return err
}
return nil
}
// ResendVerifyEmail resends an email with account activation link
func (l *UserDataCli) ResendVerifyEmail(c *cli.Context, username string) error {
if !settings.Container.Current.EnableUserVerifyEmail {
return errs.ErrEmailValidationNotAllowed
}
if username == "" {
log.BootErrorf("[user_data.ResendVerifyEmail] user name is empty")
return errs.ErrUsernameIsEmpty
}
user, err := l.users.GetUserByUsername(nil, username)
if err != nil {
log.BootErrorf("[user_data.ResendVerifyEmail] failed to get user by user name \"%s\", because %s", username, err.Error())
return err
}
if user.EmailVerified {
log.BootWarnf("[user_data.ResendVerifyEmail] user \"uid:%d\" email has been verified", user.Uid)
return errs.ErrEmailIsVerified
}
token, _, err := l.tokens.CreateEmailVerifyToken(nil, user)
if err != nil {
log.BootErrorf("[user_data.ResendVerifyEmail] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return errs.ErrTokenGenerating
}
err = l.users.SendVerifyEmail(user, token, "")
if err != nil {
log.BootErrorf("[user_data.ResendVerifyEmail] cannot send email to \"%s\", because %s", user.Email, err.Error())
return err
}
return nil
}
// SetUserEmailVerified sets user email address verified
func (l *UserDataCli) SetUserEmailVerified(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.SetUserEmailVerified] user name is empty")
return errs.ErrUsernameIsEmpty
}
err := l.users.SetUserEmailVerified(nil, username)
if err != nil {
log.BootErrorf("[user_data.SetUserEmailVerified] failed to set user email address verified by user name \"%s\", because %s", username, err.Error())
return err
}
return nil
}
// SetUserEmailUnverified sets user email address unverified
func (l *UserDataCli) SetUserEmailUnverified(c *cli.Context, username string) error {
if username == "" {
log.BootErrorf("[user_data.SetUserEmailUnverified] user name is empty")
return errs.ErrUsernameIsEmpty
}
err := l.users.SetUserEmailUnverified(nil, username)
if err != nil {
log.BootErrorf("[user_data.SetUserEmailUnverified] failed to set user email address unverified by user name \"%s\", because %s", username, err.Error())
return err
}
return nil
}
// DeleteUser deletes user according to the specified user name
func (l *UserDataCli) DeleteUser(c *cli.Context, username string) error {
if username == "" {
@@ -168,7 +317,7 @@ func (l *UserDataCli) DeleteUser(c *cli.Context, username string) error {
return errs.ErrUsernameIsEmpty
}
err := l.users.DeleteUser(username)
err := l.users.DeleteUser(nil, username)
if err != nil {
log.BootErrorf("[user_data.DeleteUser] failed to delete user by user name \"%s\", because %s", username, err.Error())
@@ -192,7 +341,7 @@ func (l *UserDataCli) ListUserTokens(c *cli.Context, username string) ([]*models
return nil, err
}
tokens, err := l.tokens.GetAllUnexpiredNormalTokensByUid(uid)
tokens, err := l.tokens.GetAllUnexpiredNormalTokensByUid(nil, uid)
if err != nil {
log.BootErrorf("[user_data.ListUserTokens] failed to get tokens of user \"%s\", because %s", username, err.Error())
@@ -217,7 +366,7 @@ func (l *UserDataCli) ClearUserTokens(c *cli.Context, username string) error {
}
now := time.Now().Unix()
err = l.tokens.DeleteTokensBeforeTime(uid, now)
err = l.tokens.DeleteTokensBeforeTime(nil, uid, now)
if err != nil {
log.BootErrorf("[user_data.ClearUserTokens] failed to delete tokens of user \"%s\", because %s", username, err.Error())
@@ -241,10 +390,10 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
return err
}
enableTwoFactor, err := l.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
enableTwoFactor, err := l.twoFactorAuthorizations.ExistsTwoFactorSetting(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to check two factor setting, because %s", err.Error())
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to check two-factor setting, because %s", err.Error())
return err
}
@@ -252,17 +401,17 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
return errs.ErrTwoFactorIsNotEnabled
}
err = l.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
err = l.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two factor recovery codes for user \"%s\"", username)
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two-factor recovery codes for user \"%s\"", username)
return err
}
err = l.twoFactorAuthorizations.DeleteTwoFactorSetting(uid)
err = l.twoFactorAuthorizations.DeleteTwoFactorSetting(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two factor setting for user \"%s\"", username)
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two-factor setting for user \"%s\"", username)
return err
}
@@ -283,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())
@@ -298,7 +447,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
}
}
allTransactions, err := l.transactions.GetAllTransactions(uid, pageCountForGettingTransactions, false)
allTransactions, err := l.transactions.GetAllTransactions(nil, uid, pageCountForGettingTransactions, false)
if err != nil {
log.BootErrorf("[user_data.CheckTransactionAndAccount] failed to all transactions for user \"%s\", because %s", username, err.Error())
@@ -323,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
@@ -352,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
}
@@ -367,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
}
}
@@ -381,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
}
}
@@ -389,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
@@ -403,21 +627,29 @@ 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())
return nil, err
}
allTransactions, err := l.transactions.GetAllTransactions(uid, pageCountForDataExport, true)
allTransactions, err := l.transactions.GetAllTransactions(nil, uid, pageCountForDataExport, true)
if err != nil {
log.BootErrorf("[user_data.ExportTransaction] failed to all transactions for user \"%s\", because %s", username, err.Error())
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())
@@ -438,47 +670,49 @@ 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(uid)
accounts, err := l.accounts.GetAllAccountsByUid(nil, uid)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get accounts for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
accountMap = l.accounts.GetAccountMapByList(accounts)
categories, err := l.categories.GetAllCategoriesByUid(uid, 0, -1)
categories, err := l.categories.GetAllCategoriesByUid(nil, uid, 0, -1)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get categories for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
categoryMap = l.categories.GetCategoryMapByList(categories)
tags, err := l.tags.GetAllTagsByUid(uid)
tags, err := l.tags.GetAllTagsByUid(nil, uid)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get tags for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
tagMap = l.tags.GetTagMapByList(tags)
tagIndexs, err = l.tags.GetAllTagIdsOfAllTransactions(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 {
@@ -490,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
}
@@ -503,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
}
}
@@ -536,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)
}
+61 -6
View File
@@ -1,6 +1,7 @@
package core
import (
"net"
"strconv"
"github.com/gin-gonic/gin"
@@ -9,9 +10,16 @@ import (
)
const requestIdFieldKey = "REQUEST_ID"
const textualTokenFieldKey = "TOKEN_STRING"
const tokenClaimsFieldKey = "TOKEN_CLAIMS"
const responseErrorFieldKey = "RESPONSE_ERROR"
// AcceptLanguageHeaderName represents the header name of accept language
const AcceptLanguageHeaderName = "Accept-Language"
// 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"
@@ -21,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)
@@ -37,7 +75,23 @@ func (c *Context) GetRequestId() string {
return requestId.(string)
}
// SetTokenClaims sets the given user token id to context
// SetTextualToken sets the given user token to context
func (c *Context) SetTextualToken(token string) {
c.Set(textualTokenFieldKey, token)
}
// GetTextualToken returns the current user textual token
func (c *Context) GetTextualToken() string {
token, exists := c.Get(textualTokenFieldKey)
if !exists {
return ""
}
return token.(string)
}
// SetTokenClaims sets the given user token to context
func (c *Context) SetTokenClaims(claims *UserTokenClaims) {
c.Set(tokenClaimsFieldKey, claims)
}
@@ -61,13 +115,14 @@ func (c *Context) GetCurrentUid() int64 {
return 0
}
uid, err := strconv.ParseInt(claims.Id, 10, 64)
return claims.Uid
}
if err != nil {
return 0
}
// GetClientLocale returns the client locale name
func (c *Context) GetClientLocale() string {
value := c.GetHeader(AcceptLanguageHeaderName)
return uid
return value
}
// GetClientTimezoneOffset returns the client timezone offset
+13 -3
View File
@@ -1,12 +1,22 @@
package core
import "github.com/mayswind/ezbookkeeping/pkg/errs"
import (
"net/http/httputil"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
// MiddlewareHandlerFunc represents the middleware handler function
type MiddlewareHandlerFunc func(*Context)
// ApiHandlerFunc represents the api handler function
type ApiHandlerFunc func(*Context) (interface{}, *errs.Error)
type ApiHandlerFunc func(*Context) (any, *errs.Error)
// DataHandlerFunc represents the handler function that returns byte array
// 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))
}
}
+44 -4
View File
@@ -1,7 +1,9 @@
package core
import (
"github.com/dgrijalva/jwt-go"
"time"
"github.com/golang-jwt/jwt/v5"
)
// TokenType represents token type
@@ -9,14 +11,52 @@ type TokenType byte
// Token types
const (
USER_TOKEN_TYPE_NORMAL TokenType = 1
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
USER_TOKEN_TYPE_NORMAL TokenType = 1
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
USER_TOKEN_TYPE_EMAIL_VERIFY TokenType = 3
USER_TOKEN_TYPE_PASSWORD_RESET TokenType = 4
)
// UserTokenClaims represents user token
type UserTokenClaims struct {
UserTokenId string `json:"userTokenId"`
Uid int64 `json:"jti,string"`
Username string `json:"username,omitempty"`
Type TokenType `json:"type"`
jwt.StandardClaims
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
}
// GetExpirationTime returns the expiration time of this token
func (c *UserTokenClaims) GetExpirationTime() (*jwt.NumericDate, error) {
return &jwt.NumericDate{
Time: time.Unix(c.ExpiresAt, 0),
}, nil
}
// GetIssuedAt returns the issue time of this token
func (c *UserTokenClaims) GetIssuedAt() (*jwt.NumericDate, error) {
return &jwt.NumericDate{
Time: time.Unix(c.IssuedAt, 0),
}, nil
}
// GetNotBefore returns the earliest valid time of this token
func (c *UserTokenClaims) GetNotBefore() (*jwt.NumericDate, error) {
return &jwt.NumericDate{}, nil
}
// GetIssuer returns the issuer of this token
func (c *UserTokenClaims) GetIssuer() (string, error) {
return "", nil
}
// GetSubject returns the subject of this token
func (c *UserTokenClaims) GetSubject() (string, error) {
return "", nil
}
// GetAudience returns the audience of this token
func (c *UserTokenClaims) GetAudience() (jwt.ClaimStrings, error) {
return jwt.ClaimStrings{}, nil
}
+18 -4
View File
@@ -1,15 +1,29 @@
package datastore
import "xorm.io/xorm"
import (
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// Database represents a database instance
type Database struct {
*xorm.EngineGroup
engineGroup *xorm.EngineGroup
}
// NewSession starts a new session with the specified context
func (db *Database) NewSession(c *core.Context) *xorm.Session {
return db.engineGroup.Context(NewXOrmContextAdapter(c))
}
// DoTransaction runs a new database transaction
func (db *Database) DoTransaction(fn func(sess *xorm.Session) error) (err error) {
sess := db.NewSession()
func (db *Database) DoTransaction(c *core.Context, fn func(sess *xorm.Session) error) (err error) {
sess := db.engineGroup.NewSession()
if c != nil {
sess.Context(NewXOrmContextAdapter(c))
}
defer sess.Close()
if err = sess.Begin(); err != nil {
+7 -6
View File
@@ -3,6 +3,7 @@ package datastore
import (
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
@@ -17,21 +18,21 @@ func (s *DataStore) Choose(key int64) *Database {
}
// Query returns a new database session in a specific database by sharding key
func (s *DataStore) Query(key int64) *xorm.Session {
return s.Choose(key).NewSession()
func (s *DataStore) Query(c *core.Context, key int64) *xorm.Session {
return s.Choose(key).NewSession(c)
}
// DoTransaction runs a new database transaction in a specific database by sharding key
func (s *DataStore) DoTransaction(key int64, fn func(sess *xorm.Session) error) (err error) {
return s.Choose(key).DoTransaction(fn)
func (s *DataStore) DoTransaction(key int64, c *core.Context, fn func(sess *xorm.Session) error) (err error) {
return s.Choose(key).DoTransaction(c, fn)
}
// SyncStructs updates database structs by database models
func (s *DataStore) SyncStructs(beans ...interface{}) error {
func (s *DataStore) SyncStructs(beans ...any) error {
var err error
for i := 0; i < len(s.databases); i++ {
err = s.databases[i].Sync2(beans...)
err = s.databases[i].engineGroup.Sync2(beans...)
if err != nil {
return err
+8 -11
View File
@@ -2,6 +2,7 @@ package datastore
import (
"fmt"
"net"
"net/url"
"os"
"strings"
@@ -98,19 +99,19 @@ func initializeDatabase(dbConfig *settings.DatabaseConfig) (*Database, error) {
return nil, err
}
engineGroup.SetMaxIdleConns(dbConfig.MaxIdleConnection)
engineGroup.SetMaxOpenConns(dbConfig.MaxOpenConnection)
engineGroup.SetMaxIdleConns(int(dbConfig.MaxIdleConnection))
engineGroup.SetMaxOpenConns(int(dbConfig.MaxOpenConnection))
engineGroup.SetConnMaxLifetime(time.Duration(dbConfig.ConnectionMaxLifeTime) * time.Second)
return &Database{
EngineGroup: engineGroup,
engineGroup: engineGroup,
}, nil
}
func setDatabaseLogger(database *Database, config *settings.Config) {
if config.EnableQueryLog {
database.SetLogger(NewXOrmLoggerAdapter(config.EnableQueryLog, config.LogLevel))
database.ShowSQL(true)
database.engineGroup.SetLogger(NewXOrmLoggerAdapter(config.EnableQueryLog, config.LogLevel))
database.engineGroup.ShowSQL(true)
}
}
@@ -126,16 +127,12 @@ func getMysqlConnectionString(dbConfig *settings.DatabaseConfig) (string, error)
}
func getPostgresConnectionString(dbConfig *settings.DatabaseConfig) (string, error) {
host, port := "", ""
fields := strings.Split(dbConfig.DatabaseHost, ":")
host, port, err := net.SplitHostPort(dbConfig.DatabaseHost)
if len(fields) != 2 {
if err != nil {
return "", errs.ErrDatabaseHostInvalid
}
host = strings.TrimSpace(fields[0])
port = strings.TrimSpace(fields[1])
if strings.HasPrefix(dbConfig.DatabaseHost, "/") { // unix socket path
return fmt.Sprintf("postgres://%s:%s@:%s/%s?sslmode=%s&host=%s",
url.QueryEscape(dbConfig.DatabaseUser), url.QueryEscape(dbConfig.DatabasePassword), port, dbConfig.DatabaseName, dbConfig.DatabaseSSLMode, host), nil
+50
View File
@@ -0,0 +1,50 @@
package datastore
import (
"fmt"
"time"
"xorm.io/xorm/log"
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// XOrmContextAdapter represents the context adapter for xorm
type XOrmContextAdapter struct {
requestId string
}
// Deadline does nothing
func (c *XOrmContextAdapter) Deadline() (deadline time.Time, ok bool) {
return
}
// Done always returns nil
func (c *XOrmContextAdapter) Done() <-chan struct{} {
return nil
}
// Err always returns nil
func (c *XOrmContextAdapter) Err() error {
return nil
}
// Value returns the value associated with this context for key, or nil
// if no value is associated with key.
func (c *XOrmContextAdapter) Value(key any) any {
if key == log.SessionIDKey && c.requestId != "" {
return fmt.Sprintf("%s", c.requestId)
}
return nil
}
func NewXOrmContextAdapter(c *core.Context) *XOrmContextAdapter {
if c != nil {
return &XOrmContextAdapter{
requestId: c.GetRequestId(),
}
}
return &XOrmContextAdapter{}
}
+8 -8
View File
@@ -14,42 +14,42 @@ type XOrmLoggerAdapter struct {
}
// Debug logs debug log
func (logger XOrmLoggerAdapter) Debug(v ...interface{}) {
func (logger XOrmLoggerAdapter) Debug(v ...any) {
log.SqlQuery(v...)
}
// Debugf logs debug log with custom format
func (logger XOrmLoggerAdapter) Debugf(format string, v ...interface{}) {
func (logger XOrmLoggerAdapter) Debugf(format string, v ...any) {
log.SqlQueryf(format, v...)
}
// Info logs info log
func (logger XOrmLoggerAdapter) Info(v ...interface{}) {
func (logger XOrmLoggerAdapter) Info(v ...any) {
log.SqlQuery(v...)
}
// Infof logs info log with custom format
func (logger XOrmLoggerAdapter) Infof(format string, v ...interface{}) {
func (logger XOrmLoggerAdapter) Infof(format string, v ...any) {
log.SqlQueryf(format, v...)
}
// Warn logs warn log
func (logger XOrmLoggerAdapter) Warn(v ...interface{}) {
func (logger XOrmLoggerAdapter) Warn(v ...any) {
log.SqlQuery(v...)
}
// Warnf logs warn log with custom format
func (logger XOrmLoggerAdapter) Warnf(format string, v ...interface{}) {
func (logger XOrmLoggerAdapter) Warnf(format string, v ...any) {
log.SqlQueryf(format, v...)
}
// Error logs error log
func (logger XOrmLoggerAdapter) Error(v ...interface{}) {
func (logger XOrmLoggerAdapter) Error(v ...any) {
log.SqlQuery(v...)
}
// Errorf logs error log with custom format
func (logger XOrmLoggerAdapter) Errorf(format string, v ...interface{}) {
func (logger XOrmLoggerAdapter) Errorf(format string, v ...any) {
log.SqlQueryf(format, v...)
}
@@ -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")
)
+35 -8
View File
@@ -1,7 +1,7 @@
package errs
// ErrorCategory represents error category
type ErrorCategory int
type ErrorCategory int32
// Error categories
const (
@@ -14,6 +14,8 @@ const (
SystemSubcategoryDefault = 0
SystemSubcategorySetting = 1
SystemSubcategoryDatabase = 2
SystemSubcategoryMail = 3
SystemSubcategoryLogging = 4
)
// Sub categories of normal error
@@ -27,16 +29,19 @@ const (
NormalSubcategoryCategory = 6
NormalSubcategoryTag = 7
NormalSubcategoryDataManagement = 8
NormalSubcategoryMapProxy = 9
NormalSubcategoryTemplate = 10
)
// Error represents the specific error returned to user
type Error struct {
Category ErrorCategory
SubCategory int
Index int
SubCategory int32
Index int32
HttpStatusCode int
Message string
BaseError []error
Context any
}
// Error returns the error message
@@ -45,12 +50,12 @@ func (err *Error) Error() string {
}
// Code returns the error code
func (err *Error) Code() int {
return int(err.Category)*100000 + err.SubCategory*1000 + err.Index
func (err *Error) Code() int32 {
return int32(err.Category)*100000 + err.SubCategory*1000 + err.Index
}
// New returns a new error instance
func New(category ErrorCategory, subCategory int, index int, httpStatusCode int, message string, baseError ...error) *Error {
func New(category ErrorCategory, subCategory int32, index int32, httpStatusCode int, message string, baseError ...error) *Error {
return &Error{
Category: category,
SubCategory: subCategory,
@@ -62,15 +67,24 @@ func New(category ErrorCategory, subCategory int, index int, httpStatusCode int,
}
// NewSystemError returns a new system error instance
func NewSystemError(subCategory int, index int, httpStatusCode int, message string) *Error {
func NewSystemError(subCategory int32, index int32, httpStatusCode int, message string) *Error {
return New(CATEGORY_SYSTEM, subCategory, index, httpStatusCode, message)
}
// NewNormalError returns a new normal error instance
func NewNormalError(subCategory int, index int, httpStatusCode int, message string) *Error {
func NewNormalError(subCategory int32, index int32, httpStatusCode int, message string) *Error {
return New(CATEGORY_NORMAL, subCategory, index, httpStatusCode, message)
}
// 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,
@@ -80,6 +94,19 @@ func NewIncompleteOrIncorrectSubmissionError(err error) *Error {
ErrIncompleteOrIncorrectSubmission.Message, err)
}
// NewErrorWithContext returns a new error instance with specified context
func NewErrorWithContext(baseError *Error, context any) *Error {
return &Error{
Category: baseError.Category,
SubCategory: baseError.SubCategory,
Index: baseError.Index,
HttpStatusCode: baseError.HttpStatusCode,
Message: baseError.Message,
BaseError: baseError.BaseError,
Context: context,
}
}
// Or would return the error from err parameter if the this error is defined in this project,
// or return the default error
func Or(err error, defaultErr *Error) *Error {
+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 mail
var (
ErrSMTPServerNotEnabled = NewSystemError(SystemSubcategoryMail, 0, http.StatusInternalServerError, "SMTP server is not enabled")
ErrSMTPServerHostInvalid = NewSystemError(SystemSubcategoryMail, 1, http.StatusInternalServerError, "SMTP server host is invalid")
)
+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 -5
View File
@@ -4,9 +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")
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 -4
View File
@@ -4,8 +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")
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")
)
+15 -12
View File
@@ -6,16 +6,19 @@ import (
// Error codes related to tokens
var (
ErrTokenGenerating = NewNormalError(NormalSubcategoryToken, 0, http.StatusInternalServerError, "failed to generate token")
ErrUnauthorizedAccess = NewNormalError(NormalSubcategoryToken, 1, http.StatusUnauthorized, "unauthorized access")
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two factor authorization")
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two factor authorization")
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user token id is invalid")
ErrTokenRecordNotFound = NewNormalError(NormalSubcategoryToken, 10, http.StatusBadRequest, "token is not found")
ErrTokenExpired = NewNormalError(NormalSubcategoryToken, 11, http.StatusBadRequest, "token is expired")
ErrTokenGenerating = NewNormalError(NormalSubcategoryToken, 0, http.StatusInternalServerError, "failed to generate token")
ErrUnauthorizedAccess = NewNormalError(NormalSubcategoryToken, 1, http.StatusUnauthorized, "unauthorized access")
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two-factor authorization")
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two-factor authorization")
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user token id is invalid")
ErrTokenRecordNotFound = NewNormalError(NormalSubcategoryToken, 10, http.StatusBadRequest, "token is not found")
ErrTokenExpired = NewNormalError(NormalSubcategoryToken, 11, http.StatusBadRequest, "token is expired")
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
ErrEmailVerifyTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 13, http.StatusBadRequest, "email verify token is invalid or expired")
ErrPasswordResetTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 14, http.StatusBadRequest, "password reset token is invalid or expired")
)
+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 -15
View File
@@ -6,19 +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")
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")
)
@@ -33,7 +33,7 @@ type BankOfCanadaExchangeRateData struct {
}
// BankOfCanadaObservationData represents the observation data from bank of Canada
type BankOfCanadaObservationData map[string]interface{}
type BankOfCanadaObservationData map[string]any
// ToLatestExchangeRateResponse returns a view-object according to original data from bank of Canada
func (e *BankOfCanadaExchangeRateData) ToLatestExchangeRateResponse(c *core.Context) *models.LatestExchangeRateResponse {
@@ -62,7 +62,7 @@ func (e *BankOfCanadaExchangeRateData) ToLatestExchangeRateResponse(c *core.Cont
currencyCode := utils.SubString(typeName, 2, 3)
if data, ok := exchangeRateData.(map[string]interface{}); ok {
if data, ok := exchangeRateData.(map[string]any); ok {
exchangeRate := data["v"]
if exchangeRateValue, ok2 := exchangeRate.(string); ok2 {
@@ -18,7 +18,7 @@ const euroCentralBankDataSource = "European Central Bank"
const euroCentralBankBaseCurrency = "EUR"
const euroCentralBankDataUpdateDateFormat = "2006-01-02 15"
const euroCentralBankDataUpdateDateTimezone = "Etc/GMT-1" // UTC+01:00
const euroCentralBankDataUpdateDateTimezone = "Europe/Berlin"
// EuroCentralBankDataSource defines the structure of exchange rates data source of euro central bank
type EuroCentralBankDataSource struct {
@@ -32,6 +32,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error {
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
Container.Current = &NationalBankOfPolandDataSource{}
return nil
} else if config.ExchangeRatesDataSource == settings.MonetaryAuthorityOfSingaporeDataSource {
Container.Current = &MonetaryAuthorityOfSingaporeDataSource{}
return nil
}
return errs.ErrInvalidExchangeRatesDataSource
@@ -0,0 +1,179 @@
package exchangerates
import (
"encoding/json"
"math"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const monetaryAuthorityOfSingaporeExchangeRateUrl = "https://eservices.mas.gov.sg/api/action/datastore/search.json?resource_id=95932927-c8bc-4e7a-b484-68a66a24edfe&sort=end_of_day+desc&limit=1"
const monetaryAuthorityOfSingaporeExchangeRateReferenceUrl = "https://eservices.mas.gov.sg/Statistics/msb/ExchangeRates.aspx"
const monetaryAuthorityOfSingaporeDataSource = "Monetary Authority of Singapore"
const monetaryAuthorityOfSingaporeBaseCurrency = "SGD"
const monetaryAuthorityOfSingaporeDataUpdateDateFormat = "2006-01-02 15"
const monetaryAuthorityOfSingaporeDataUpdateDateTimezone = "Asia/Singapore"
// MonetaryAuthorityOfSingaporeDataSource defines the structure of exchange rates data source of Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeDataSource struct {
ExchangeRatesDataSource
}
// MonetaryAuthorityOfSingaporeExchangeRateData represents the whole data from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeExchangeRateData struct {
Success bool `json:"success"`
Result *MonetaryAuthorityOfSingaporeResult `json:"result"`
}
// MonetaryAuthorityOfSingaporeResult represents the actual result from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeResult struct {
Records []MonetaryAuthorityOfSingaporeRecord `json:"records"`
}
// MonetaryAuthorityOfSingaporeRecord represents the record from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeRecord map[string]string
// ToLatestExchangeRateResponse returns a view-object according to original data from Monetary Authority of Singapore
func (e *MonetaryAuthorityOfSingaporeExchangeRateData) ToLatestExchangeRateResponse(c *core.Context) *models.LatestExchangeRateResponse {
if !e.Success {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] response is not success")
return nil
}
if e.Result == nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] result is null")
return nil
}
if len(e.Result.Records) < 1 {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] records is empty")
return nil
}
lastDayRecord := e.Result.Records[0]
exchangeRates := make(models.LatestExchangeRateSlice, 0, len(lastDayRecord))
latestUpdateDate := ""
for key, value := range lastDayRecord {
if key == "end_of_day" {
latestUpdateDate = value
continue
}
exchangeRate := e.parseExchangeRateResponse(c, key, value)
if exchangeRate == nil {
continue
}
exchangeRates = append(exchangeRates, exchangeRate)
}
timezone, err := time.LoadLocation(monetaryAuthorityOfSingaporeDataUpdateDateTimezone)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] failed to get timezone, timezone name is %s", monetaryAuthorityOfSingaporeDataUpdateDateTimezone)
return nil
}
updateDateTime := latestUpdateDate + " 12" // These rates are the average of buying and selling interbank rates quoted around midday in Singapore
updateTime, err := time.ParseInLocation(monetaryAuthorityOfSingaporeDataUpdateDateFormat, updateDateTime, timezone)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] failed to parse update date, datetime is %s", updateDateTime)
return nil
}
latestExchangeRateResp := &models.LatestExchangeRateResponse{
DataSource: monetaryAuthorityOfSingaporeDataSource,
ReferenceUrl: monetaryAuthorityOfSingaporeExchangeRateReferenceUrl,
UpdateTime: updateTime.Unix(),
BaseCurrency: monetaryAuthorityOfSingaporeBaseCurrency,
ExchangeRates: exchangeRates,
}
return latestExchangeRateResp
}
func (e *MonetaryAuthorityOfSingaporeExchangeRateData) parseExchangeRateResponse(c *core.Context, key string, value string) *models.LatestExchangeRate {
if !strings.Contains(key, "_") {
return nil
}
items := strings.Split(key, "_")
if len(items) < 2 {
return nil
}
fromCurrencyCode := strings.ToUpper(items[0])
toCurrencyCode := strings.ToUpper(items[1])
if _, exists := validators.AllCurrencyNames[fromCurrencyCode]; !exists {
return nil
}
if toCurrencyCode != monetaryAuthorityOfSingaporeBaseCurrency {
return nil
}
rate, err := utils.StringToFloat64(value)
if err != nil {
log.WarnfWithRequestId(c, "[monetary_authority_of_singapore_datasource.parseExchangeRateResponse] failed to parse rate, rate is %s", value)
return nil
}
if rate <= 0 {
log.WarnfWithRequestId(c, "[monetary_authority_of_singapore_datasource.parseExchangeRateResponse] rate is invalid, rate is %s", value)
return nil
}
finalRate := 1 / rate
if math.IsInf(finalRate, 0) {
return nil
}
if len(items) == 3 && items[2] == "100" {
finalRate = finalRate * 100
}
return &models.LatestExchangeRate{
Currency: fromCurrencyCode,
Rate: utils.Float64ToString(finalRate),
}
}
// GetRequestUrls returns the Monetary Authority of Singapore data source urls
func (e *MonetaryAuthorityOfSingaporeDataSource) GetRequestUrls() []string {
return []string{monetaryAuthorityOfSingaporeExchangeRateUrl}
}
// Parse returns the common response entity according to the Monetary Authority of Singapore data source raw response
func (e *MonetaryAuthorityOfSingaporeDataSource) Parse(c *core.Context, content []byte) (*models.LatestExchangeRateResponse, error) {
monetaryAuthorityOfSingaporeData := &MonetaryAuthorityOfSingaporeExchangeRateData{}
err := json.Unmarshal(content, monetaryAuthorityOfSingaporeData)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.Parse] failed to parse json data, content is %s, because %s", string(content), err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
latestExchangeRateResponse := monetaryAuthorityOfSingaporeData.ToLatestExchangeRateResponse(c)
if latestExchangeRateResponse == nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.Parse] failed to parse latest exchange rate data, content is %s", string(content))
return nil, errs.ErrFailedToRequestRemoteApi
}
return latestExchangeRateResponse, nil
}
@@ -0,0 +1,206 @@
package exchangerates
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/gin-gonic/gin"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const monetaryAuthorityOfSingaporeMinimumRequiredContent = "{\n" +
" \"success\": true,\n" +
" \"result\": {\n" +
" \"records\": [\n" +
" {\n" +
" \"end_of_day\": \"2023-05-26\",\n" +
" \"usd_sgd\": \"1.3528\",\n" +
" \"cny_sgd_100\": \"19.16\"\n" +
" }\n" +
" ]\n" +
" }\n" +
"}"
func TestMonetaryAuthorityOfSingaporeDataSource_StandardDataExtractBaseCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(monetaryAuthorityOfSingaporeMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Equal(t, "SGD", actualLatestExchangeRateResponse.BaseCurrency)
}
func TestMonetaryAuthorityOfSingaporeDataSource_StandardDataExtractExchangeRates(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(monetaryAuthorityOfSingaporeMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "USD",
Rate: "0.7392075694855116",
})
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "CNY",
Rate: "5.219206680584551",
})
}
func TestMonetaryAuthorityOfSingaporeDataSource_BlankContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte(""))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyJsonObject(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_ResponseSuccessIsFalseObject(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": false,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": \"1.3528\",\n"+
" \"cny_sgd_100\": \"19.16\"\n"+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_NoResultContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyRecordContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" ]\n"+
" }\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_TargetCurrencyIsNotBaseCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_cny\": \"1\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_InvalidCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"xxx_sgd\": \"1.3528\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyRate(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": \"\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_InvalidRate(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": null"+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
@@ -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)
}
+51
View File
@@ -0,0 +1,51 @@
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,
},
"zh-Hans": {
Content: zhHans,
},
}
func GetLocaleTextItems(locale string) *LocaleTextItems {
localeInfo, exists := AllLanguages[locale]
if exists {
return localeInfo.Content
}
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)
}
+35
View File
@@ -0,0 +1,35 @@
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
SalutationFormat string
DescriptionAboveBtn string
VerifyEmail string
DescriptionBelowBtnFormat string
}
// ForgetPasswordMailTextItems represents text items need to be translated in forget password mail
type ForgetPasswordMailTextItems struct {
Title string
SalutationFormat string
DescriptionAboveBtn string
ResetPassword string
DescriptionBelowBtnFormat string
}
+26
View File
@@ -0,0 +1,26 @@
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,",
DescriptionAboveBtn: "Please click the link below to confirm your email address.",
VerifyEmail: "Verify Email",
DescriptionBelowBtnFormat: "If you did not sign up for %s account, please simply disregard this email. If you cannot click the link above, please copy the above url and paste it into your browser. The verify email link will be expired after %v minutes.",
},
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
Title: "Reset Your Password",
SalutationFormat: "Hi %s,",
DescriptionAboveBtn: "We recently received a request to reset your password. You can click the link below to reset your password.",
ResetPassword: "Reset Password",
DescriptionBelowBtnFormat: "If you did not request to reset your password, please simply disregard this email. If you cannot click the link above, please copy the above url and paste it into your browser. The password reset link will be expired after %v minutes.",
},
}
+7
View File
@@ -0,0 +1,7 @@
package locales
// LocaleInfo represents locale info
type LocaleInfo struct {
Aliases []string
Content *LocaleTextItems
}
+26
View File
@@ -0,0 +1,26 @@
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 您好,",
DescriptionAboveBtn: "请点击下方的链接确认您的邮箱地址。",
VerifyEmail: "验证邮箱",
DescriptionBelowBtnFormat: "如果您没有注册 %s 账户,请直接忽略本邮件。如果您无法点击上述链接,请复制下方的地址然后在您的浏览器中粘贴。邮箱验证链接将在 %v 分钟后过期。",
},
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
Title: "重置密码",
SalutationFormat: "%s 您好,",
DescriptionAboveBtn: "我们刚才收到重置您密码的请求。您可以点击下方链接重置您的密码。",
ResetPassword: "重置密码",
DescriptionBelowBtnFormat: "如果您没有请求重置密码,请直接忽略本邮件。如果您无法点击上述链接,请复制下方的地址然后在您的浏览器中粘贴。重置密码链接将在 %v 分钟后过期。",
},
}
+3 -3
View File
@@ -41,12 +41,12 @@ func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
b.WriteString("] ")
}
b.WriteString(entry.Message)
if requestId, exists := entry.Data[logFieldRequestId]; exists {
b.WriteString(fmt.Sprintf(", r=%s", requestId))
b.WriteString(fmt.Sprintf("[%s] ", requestId))
}
b.WriteString(entry.Message)
b.WriteString("\n")
if extra, exists := entry.Data[logFieldExtra]; exists {
+80 -41
View File
@@ -39,34 +39,73 @@ func init() {
}
// SetLoggerConfiguration sets the logger according to the config
func SetLoggerConfiguration(config *settings.Config) error {
func SetLoggerConfiguration(config *settings.Config, isDisableBootLog bool) error {
var bootWriters []io.Writer
var writers []io.Writer
var defaultWriters []io.Writer
var requestWriters []io.Writer
var queryWriters []io.Writer
bootWriters = append(bootWriters, os.Stdout)
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
}
bootWriters = append(bootWriters, logFile)
writers = append(writers, logFile)
if !isDisableBootLog {
bootWriters = append(bootWriters, defaultWriter)
}
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)
@@ -94,93 +133,93 @@ func SetLoggerConfiguration(config *settings.Config) error {
}
// Debugf logs debug log with custom format
func Debugf(format string, args ...interface{}) {
defaultLogger.Debugf(getFinalLog(format, args...))
func Debugf(format string, args ...any) {
defaultLogger.Debug(getFinalLog(format, args...))
}
// DebugfWithRequestId logs debug log with custom format and request id
func DebugfWithRequestId(c *core.Context, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Debugf(getFinalLog(format, args...))
func DebugfWithRequestId(c *core.Context, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Debug(getFinalLog(format, args...))
}
// Infof logs info log with custom format
func Infof(format string, args ...interface{}) {
defaultLogger.Infof(getFinalLog(format, args...))
func Infof(format string, args ...any) {
defaultLogger.Info(getFinalLog(format, args...))
}
// InfofWithRequestId logs info log with custom format and request id
func InfofWithRequestId(c *core.Context, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Infof(getFinalLog(format, args...))
func InfofWithRequestId(c *core.Context, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Info(getFinalLog(format, args...))
}
// Warnf logs warn log with custom format
func Warnf(format string, args ...interface{}) {
defaultLogger.Warnf(getFinalLog(format, args...))
func Warnf(format string, args ...any) {
defaultLogger.Warn(getFinalLog(format, args...))
}
// WarnfWithRequestId logs warn log with custom format and request id
func WarnfWithRequestId(c *core.Context, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Warnf(getFinalLog(format, args...))
func WarnfWithRequestId(c *core.Context, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Warn(getFinalLog(format, args...))
}
// Errorf logs error log with custom format
func Errorf(format string, args ...interface{}) {
defaultLogger.Errorf(getFinalLog(format, args...))
func Errorf(format string, args ...any) {
defaultLogger.Error(getFinalLog(format, args...))
}
// ErrorfWithRequestId logs error log with custom format and request id
func ErrorfWithRequestId(c *core.Context, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Errorf(getFinalLog(format, args...))
func ErrorfWithRequestId(c *core.Context, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Error(getFinalLog(format, args...))
}
// ErrorfWithRequestIdAndExtra logs error log with custom format and request id and extra info
func ErrorfWithRequestIdAndExtra(c *core.Context, extraString string, format string, args ...interface{}) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).WithField(logFieldExtra, extraString).Errorf(getFinalLog(format, args...))
func ErrorfWithRequestIdAndExtra(c *core.Context, extraString string, format string, args ...any) {
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).WithField(logFieldExtra, extraString).Error(getFinalLog(format, args...))
}
// BootInfof logs boot info log
func BootInfof(format string, args ...interface{}) {
func BootInfof(format string, args ...any) {
if bootLogger != nil {
bootLogger.Infof(getFinalLog(format, args...))
bootLogger.Info(getFinalLog(format, args...))
}
}
// BootWarnf logs boot warn log
func BootWarnf(format string, args ...interface{}) {
func BootWarnf(format string, args ...any) {
if bootLogger != nil {
bootLogger.Warnf(getFinalLog(format, args...))
bootLogger.Warn(getFinalLog(format, args...))
}
}
// BootErrorf logs boot error log
func BootErrorf(format string, args ...interface{}) {
func BootErrorf(format string, args ...any) {
if bootLogger != nil {
bootLogger.Errorf(getFinalLog(format, args...))
bootLogger.Error(getFinalLog(format, args...))
}
}
// Requestf logs http request log with custom format
func Requestf(c *core.Context, format string, args ...interface{}) {
func Requestf(c *core.Context, format string, args ...any) {
if requestLogger != nil {
requestLogger.WithField(logFieldRequestId, c.GetRequestId()).Infof(getFinalLog(format, args...))
requestLogger.WithField(logFieldRequestId, c.GetRequestId()).Info(getFinalLog(format, args...))
}
}
// SqlQuery logs sql query log
func SqlQuery(args ...interface{}) {
func SqlQuery(args ...any) {
if sqlQueryLogger != nil {
sqlQueryLogger.Info(args...)
}
}
// SqlQueryf logs sql query log with custom format
func SqlQueryf(format string, args ...interface{}) {
func SqlQueryf(format string, args ...any) {
if sqlQueryLogger != nil {
sqlQueryLogger.Infof(getFinalLog(format, args...))
sqlQueryLogger.Info(getFinalLog(format, args...))
}
}
func getFinalLog(format string, args ...interface{}) string {
func getFinalLog(format string, args ...any) string {
result := fmt.Sprintf(format, args...)
result = strings.Replace(result, "\n", " ", -1)
+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())
}
}
}
+63
View File
@@ -0,0 +1,63 @@
package mail
import (
"crypto/tls"
"net"
"gopkg.in/mail.v2"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// DefaultMailer represents default mailer
type DefaultMailer struct {
dialer *mail.Dialer
fromAddress string
}
// NewDefaultMailer returns a new default mailer
func NewDefaultMailer(smtpConfig *settings.SMTPConfig) (*DefaultMailer, error) {
host, portStr, err := net.SplitHostPort(smtpConfig.SMTPHost)
if err != nil {
return nil, errs.ErrSMTPServerHostInvalid
}
port, err := utils.StringToInt(portStr)
if err != nil {
return nil, errs.ErrSMTPServerHostInvalid
}
dialer := mail.NewDialer(host, port, smtpConfig.SMTPUser, smtpConfig.SMTPPasswd)
dialer.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: smtpConfig.SMTPSkipTLSVerify,
}
mailer := &DefaultMailer{
dialer: dialer,
fromAddress: smtpConfig.FromAddress,
}
return mailer, nil
}
// SendMail sends an email according to argument
func (m *DefaultMailer) SendMail(message *MailMessage) error {
if m.dialer == nil {
return errs.ErrSMTPServerNotEnabled
}
mailMessage := mail.NewMessage()
mailMessage.SetHeader("From", m.fromAddress)
mailMessage.SetHeader("To", message.To)
mailMessage.SetHeader("Subject", message.Subject)
mailMessage.SetBody("text/html", message.Body)
err := m.dialer.DialAndSend(mailMessage)
return err
}
+8
View File
@@ -0,0 +1,8 @@
package mail
// MailMessage represents an email entity
type MailMessage struct {
To string
Subject string
Body string
}
+6
View File
@@ -0,0 +1,6 @@
package mail
// Mailer is email sender interface
type Mailer interface {
SendMail(message *MailMessage) error
}
+37
View File
@@ -0,0 +1,37 @@
package mail
import (
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// MailerContainer contains the current mailer
type MailerContainer struct {
Current Mailer
}
// Initialize a mailer container singleton instance
var (
Container = &MailerContainer{}
)
// InitializeMailer initializes the current mailer according to the config
func InitializeMailer(config *settings.Config) error {
if !config.EnableSMTP {
Container.Current = nil
return nil
}
mailer, err := NewDefaultMailer(config.SMTPConfig)
if err != nil {
return err
}
Container.Current = mailer
return nil
}
// SendMail sends an email according to argument
func (u *MailerContainer) SendMail(message *MailMessage) error {
return u.Current.SendMail(message)
}
@@ -0,0 +1,19 @@
package middlewares
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const tokenCookieParam = "ebk_auth_token"
// AmapApiProxyAuthCookie adds amap api proxy auth cookie to cookies in response
func AmapApiProxyAuthCookie(c *core.Context, config *settings.Config) {
token := c.GetTextualToken()
if token != "" {
c.SetCookie(tokenCookieParam, token, int(config.TokenExpiredTime), "/_AMapService", "", false, true)
} else {
c.SetCookie(tokenCookieParam, "", -1, "/_AMapService", "", false, true)
}
}
+98 -46
View File
@@ -1,7 +1,7 @@
package middlewares
import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -10,51 +10,36 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TokenSourceType represents token source
type TokenSourceType byte
// Token source types
const (
TOKEN_SOURCE_TYPE_HEADER TokenSourceType = 1
TOKEN_SOURCE_TYPE_ARGUMENT TokenSourceType = 2
TOKEN_SOURCE_TYPE_COOKIE TokenSourceType = 3
)
const tokenQueryStringParam = "token"
// JWTAuthorization verifies whether current request is valid by jwt token
// JWTAuthorization verifies whether current request is valid by jwt token in header
func JWTAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
if err != nil {
utils.PrintJsonErrorResult(c, err)
return
}
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token requires 2fa", claims.Id)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenRequire2FA)
return
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token type is invalid", claims.Id)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
c.SetTokenClaims(claims)
c.Next()
jwtAuthorization(c, TOKEN_SOURCE_TYPE_HEADER)
}
// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token
// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token in query string
func JWTAuthorizationByQueryString(c *core.Context) {
token, exists := c.GetQuery(tokenQueryStringParam)
jwtAuthorization(c, TOKEN_SOURCE_TYPE_ARGUMENT)
}
if !exists {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorizationByQueryString] no token provided")
utils.PrintJsonErrorResult(c, errs.ErrUnauthorizedAccess)
return
}
c.Request.Header.Set("Authorization", token)
JWTAuthorization(c)
// JWTAuthorizationByCookie verifies whether current request is valid by jwt token in cookie
func JWTAuthorizationByCookie(c *core.Context) {
jwtAuthorization(c, TOKEN_SOURCE_TYPE_COOKIE)
}
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
func JWTTwoFactorAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
if err != nil {
utils.PrintJsonErrorResult(c, err)
@@ -62,7 +47,7 @@ func JWTTwoFactorAuthorization(c *core.Context) {
}
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%s\" token is not need two factor authorization", claims.Id)
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two-factor authorization", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenNotRequire2FA)
return
}
@@ -71,12 +56,74 @@ func JWTTwoFactorAuthorization(c *core.Context) {
c.Next()
}
func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
token, claims, err := services.Tokens.ParseToken(c)
// JWTEmailVerifyAuthorization verifies whether current request is email verification
func JWTEmailVerifyAuthorization(c *core.Context) {
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
if err != nil {
utils.PrintJsonErrorResult(c, errs.ErrEmailVerifyTokenIsInvalidOrExpired)
return
}
if claims.Type != core.USER_TOKEN_TYPE_EMAIL_VERIFY {
log.WarnfWithRequestId(c, "[authorization.JWTEmailVerifyAuthorization] user \"uid:%d\" token is not for email verification", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
return
}
c.SetTokenClaims(claims)
c.Next()
}
// JWTResetPasswordAuthorization verifies whether current request is password reset
func JWTResetPasswordAuthorization(c *core.Context) {
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
if err != nil {
utils.PrintJsonErrorResult(c, errs.ErrPasswordResetTokenIsInvalidOrExpired)
return
}
if claims.Type != core.USER_TOKEN_TYPE_PASSWORD_RESET {
log.WarnfWithRequestId(c, "[authorization.JWTResetPasswordAuthorization] user \"uid:%d\" token is not for password request", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
return
}
c.SetTokenClaims(claims)
c.Next()
}
func jwtAuthorization(c *core.Context, source TokenSourceType) {
claims, err := getTokenClaims(c, source)
if err != nil {
utils.PrintJsonErrorResult(c, err)
return
}
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.jwtAuthorization] user \"uid:%d\" token requires 2fa", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenRequire2FA)
return
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type is invalid", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
c.SetTokenClaims(claims)
c.Next()
}
func getTokenClaims(c *core.Context, source TokenSourceType) (*core.UserTokenClaims, *errs.Error) {
token, claims, err := parseToken(c, source)
if err != nil {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] failed to parse token, because %s", err.Error())
return nil, errs.ErrUnauthorizedAccess
return nil, errs.Or(err, errs.ErrUnauthorizedAccess)
}
if !token.Valid {
@@ -84,15 +131,20 @@ func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
return nil, errs.ErrCurrentInvalidToken
}
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is expired")
return nil, errs.ErrCurrentTokenExpired
}
if claims.Id == "" {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is empty")
if claims.Uid <= 0 {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is invalid")
return nil, errs.ErrCurrentInvalidToken
}
return claims, nil
}
func parseToken(c *core.Context, source TokenSourceType) (*jwt.Token, *core.UserTokenClaims, error) {
if source == TOKEN_SOURCE_TYPE_ARGUMENT {
return services.Tokens.ParseTokenByArgument(c, tokenQueryStringParam)
} else if source == TOKEN_SOURCE_TYPE_COOKIE {
return services.Tokens.ParseTokenByCookie(c, tokenCookieParam)
}
return services.Tokens.ParseTokenByHeader(c)
}
-16
View File
@@ -1,16 +0,0 @@
package middlewares
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
const utcOffsetQueryStringParam = "utc_offset"
// HeaderInQueryString puts some headers from query string
func HeaderInQueryString(c *core.Context) {
utcOffset, exists := c.GetQuery(utcOffsetQueryStringParam)
if exists {
c.Request.Header.Set(core.ClientTimezoneOffsetHeaderName, utcOffset)
}
}
+2 -2
View File
@@ -3,7 +3,7 @@ package middlewares
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"runtime"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -49,7 +49,7 @@ func stack(skip int) []byte {
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile {
data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
continue
+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 {
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// RequestLog logs the http request log
@@ -18,7 +19,7 @@ func RequestLog(c *core.Context) {
now := time.Now()
statusCode := c.Writer.Status()
errorCode := 0
errorCode := int32(0)
userId := "-"
claims := c.GetTokenClaims()
@@ -28,7 +29,7 @@ func RequestLog(c *core.Context) {
method := c.Request.Method
if claims != nil {
userId = claims.Id
userId = utils.Int64ToString(claims.Uid)
}
if err != nil {
+77 -1
View File
@@ -1,7 +1,9 @@
package middlewares
import (
"encoding/base64"
"fmt"
"net/url"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -15,16 +17,90 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
return func(c *core.Context) {
settingsArr := []string{
buildBooleanSetting("r", config.EnableUserRegister),
buildBooleanSetting("f", config.EnableUserForgetPassword),
buildBooleanSetting("v", config.EnableUserVerifyEmail),
buildBooleanSetting("e", config.EnableDataExport),
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
}
if config.EnableMapDataFetchProxy &&
(config.MapProvider == settings.OpenStreetMapProvider ||
config.MapProvider == settings.OpenStreetMapHumanitarianStyleProvider ||
config.MapProvider == settings.OpenTopoMapProvider ||
config.MapProvider == settings.OPNVKarteMapProvider ||
config.MapProvider == settings.CyclOSMMapProvider ||
config.MapProvider == settings.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))
}
if config.MapProvider == settings.BaiduMapProvider && config.BaiduMapAK != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("bmak", config.BaiduMapAK))
}
if config.MapProvider == settings.AmapProvider && config.AmapApplicationKey != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("amak", config.AmapApplicationKey))
}
if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod != "" {
settingsArr = append(settingsArr, buildStringSetting("amsv", strings.Replace(config.AmapSecurityVerificationMethod, "_", "", -1)))
if config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationExternalProxyMethod {
settingsArr = append(settingsArr, buildEncodedStringSetting("amep", config.AmapApiExternalProxyUrl))
}
if config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationPlainTextMethod {
settingsArr = append(settingsArr, buildEncodedStringSetting("amas", config.AmapApplicationSecret))
}
}
bundledSettings := strings.Join(settingsArr, "_")
c.SetCookie(settingsCookieName, bundledSettings, config.TokenExpiredTime, "", "", false, false)
c.SetCookie(settingsCookieName, bundledSettings, int(config.TokenExpiredTime), "", "", false, false)
c.Next()
}
}
func buildStringSetting(key string, value string) string {
return fmt.Sprintf("%s.%s", key, value)
}
func buildEncodedStringSetting(key string, value string) string {
urlEncodedValue := url.QueryEscape(value)
base64Value := base64.StdEncoding.EncodeToString([]byte(urlEncodedValue))
return fmt.Sprintf("%s.%s", key, base64Value)
}
func buildBooleanSetting(key string, value bool) string {
if value {
return fmt.Sprintf("%s.1", key)

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