Compare commits
872 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3762e6c46 | |||
| 157eb140eb | |||
| 83a0c27259 | |||
| 5c4a8e37c4 | |||
| e4faf64ea3 | |||
| a4849fa4f0 | |||
| 32155ca63d | |||
| 9ea3327517 | |||
| 946a7810a7 | |||
| ea8021b359 | |||
| caa27841ef | |||
| d1cd13723a | |||
| 0e946a4b3b | |||
| 051c319890 | |||
| f2baa4ae65 | |||
| 05a93667eb | |||
| c137156c97 | |||
| 8f8a94cd66 | |||
| 4cdb599bf3 | |||
| 77a5ccd796 | |||
| 99ae18d06d | |||
| 90318d5690 | |||
| f133692002 | |||
| 0b17251f94 | |||
| 24724bb19f | |||
| b23d630daa | |||
| fcb954ff40 | |||
| 889225301c | |||
| 816d0e7ceb | |||
| f2c0ffab99 | |||
| f1f61a9038 | |||
| 77439f675b | |||
| c57f17233a | |||
| c91a56547f | |||
| 10dc2d1713 | |||
| d3c8a520ca | |||
| 681f888529 | |||
| bce8c23b05 | |||
| 788bfa7d4b | |||
| 6d331c873b | |||
| a03df7ed36 | |||
| b0b330903c | |||
| 4e16f963a8 | |||
| 8ef4b5537c | |||
| 8273e06e43 | |||
| b91305c490 | |||
| de086aa29e | |||
| 4c69243bef | |||
| 57d8edea0a | |||
| a098c100d7 | |||
| 8fb209440d | |||
| d05736d0eb | |||
| c92a9e61b0 | |||
| 2e04affb00 | |||
| 731b6e8bad | |||
| 2b63e50837 | |||
| e22f512f22 | |||
| 9a83393290 | |||
| 30ebe49875 | |||
| e15a850cfe | |||
| 34ebc06a8d | |||
| a86428cc4d | |||
| efce4cc04e | |||
| a8484cfcaf | |||
| cc55b98e80 | |||
| 0620194c78 | |||
| a00a67f3d1 | |||
| 6b9ad1a1c8 | |||
| 3d0b993c45 | |||
| dc74ac0d0b | |||
| 7ed923b347 | |||
| 1f63aa8cdf | |||
| 84f7eab95d | |||
| 7d6c7f49e5 | |||
| 021e523d63 | |||
| 266dafa4a9 | |||
| 579c903398 | |||
| 5d2e880bc5 | |||
| 8085f7cf11 | |||
| aea4cf7e8b | |||
| 3d56bfa114 | |||
| a7280bf7ed | |||
| 085f9817fc | |||
| fcca77bca5 | |||
| 4bf2e94a9d | |||
| 95c2494545 | |||
| 7662e0eb02 | |||
| 9f438dd648 | |||
| 08a1f3a5f7 | |||
| 26d77de67a | |||
| 4f9ab9db75 | |||
| 0b83518921 | |||
| 0f8de8d699 | |||
| aae23c285e | |||
| daf73dc964 | |||
| bc0893b518 | |||
| a87bda09f7 | |||
| aa7652279e | |||
| d86dea4081 | |||
| 15d476fd40 | |||
| 453a9ff61c | |||
| 26e9a0ef2a | |||
| 7849b2f05c | |||
| 2cbcc40ca9 | |||
| 184dad8185 | |||
| 46a7cd441f | |||
| 55249e07a3 | |||
| d4850b4a18 | |||
| 93819d5894 | |||
| 4a4cec3d69 | |||
| 432993121c | |||
| 1ce0c62c30 | |||
| b1343ba92a | |||
| 84a96d80b7 | |||
| 4b249a0ebb | |||
| 58de308f30 | |||
| f151eb6197 | |||
| 9eaac329b9 | |||
| a33123022f | |||
| 3eac9af403 | |||
| a371058096 | |||
| ee003160e5 | |||
| 847349dcbd | |||
| a2d6aff28b | |||
| cc3e1f2978 | |||
| dc9bf1a1d8 | |||
| 32af32d02e | |||
| d91c99c177 | |||
| d0e8419b2e | |||
| dad3f1041e | |||
| 7b70b2db29 | |||
| e5a04596e1 | |||
| 297f8b9987 | |||
| 9eddab3dd8 | |||
| ec97d2df91 | |||
| 3dd39defc1 | |||
| c0cc9b5247 | |||
| dc13afc071 | |||
| 628bd48f73 | |||
| c827675e14 | |||
| 09fb474921 | |||
| 7e1338e081 | |||
| 7a5d7337cd | |||
| b80041433c | |||
| dd32ab83cb | |||
| c8db448dfc | |||
| 51edca66d1 | |||
| bb5529098f | |||
| d5a54dd1fb | |||
| 329119fc3b | |||
| e43cf26bb5 | |||
| 93bc3bf94b | |||
| 675b5f039a | |||
| b3e4e39b49 | |||
| 4dc072f56d | |||
| b5d72c89f2 | |||
| d2b3900ed4 | |||
| 4e44553c07 | |||
| ec6b5fb155 | |||
| 16e53feaf4 | |||
| 182bdb34cd | |||
| 59d03b54d7 | |||
| 445969c449 | |||
| 29214a3bf3 | |||
| d979dc3535 | |||
| 399413a270 | |||
| d9c8142c51 | |||
| c951285049 | |||
| 0e372969b3 | |||
| 2d51f7b2be | |||
| 02a5dcf9ba | |||
| 9a70cfe24c | |||
| 023d875a00 | |||
| 640f74c612 | |||
| 768b005200 | |||
| fb315127f9 | |||
| 756e6cac5a | |||
| daae5b68cd | |||
| 0e391bee50 | |||
| 9627e65d6d | |||
| 830974500b | |||
| 0438964e17 | |||
| 226d44651f | |||
| db71ac5279 | |||
| 7e2a0b1483 | |||
| a219444953 | |||
| 5fec41055e | |||
| 5107e451d8 | |||
| 3b34cdbda2 | |||
| 35fcd32a96 | |||
| cf325b4267 | |||
| 860ef610b7 | |||
| 5c5e6a60a7 | |||
| 435c38fb27 | |||
| d640b7a5f5 | |||
| ae25b4eb4e | |||
| 315a686fed | |||
| 4dd79b07d9 | |||
| e88d803232 | |||
| 1489854444 | |||
| c5f9e276b4 | |||
| b2a8b359d1 | |||
| 72f6a9e9a3 | |||
| 809172bf34 | |||
| c34887240e | |||
| f041e7cb7d | |||
| 5eca777891 | |||
| a9e3b79eb1 | |||
| 0a376217c6 | |||
| 687d062bfc | |||
| e123498895 | |||
| a917d16c26 | |||
| 0884af038d | |||
| 72619f3dad | |||
| cf120dbcbf | |||
| 9906f1b1a7 | |||
| ea32bfa5fc | |||
| 14f6de8af1 | |||
| 176d5a15c5 | |||
| cfc7a5bd49 | |||
| 4b68ccf678 | |||
| cb57b216d0 | |||
| 222951139c | |||
| 821c7bfbd3 | |||
| 722c1d7917 | |||
| 4d065bc724 | |||
| 2a2cb3acc9 | |||
| 4a16b82700 | |||
| ea97f8cf7a | |||
| 28f113d992 | |||
| 46caf46ef7 | |||
| 185758b638 | |||
| 1e047aed80 | |||
| 8ef7676b4f | |||
| b2fc24d5ae | |||
| 065cd9dff8 | |||
| 532c762553 | |||
| 4857055eaf | |||
| 604379bf85 | |||
| 3d0f793cf6 | |||
| 9f8634ac11 | |||
| 6ab70b97b4 | |||
| 8c164ec833 | |||
| 09d47459b6 | |||
| 2916106f27 | |||
| 6b4292596a | |||
| 78aebb7d6c | |||
| 771bb35e9f | |||
| 453ed4227d | |||
| 88a284a21b | |||
| 9488b85705 | |||
| 8be3fd7ed4 | |||
| 8fdc0019ea | |||
| b21dd73ff2 | |||
| 24432701dd | |||
| 802cdabd75 | |||
| 8c83af1543 | |||
| deb465377e | |||
| bed2aef7f8 | |||
| 57b2b2492f | |||
| 87cdaee547 | |||
| a8b0ef2483 | |||
| 63e976e5cb | |||
| f73830d37b | |||
| 6511c4e810 | |||
| 008c58f52b | |||
| fa4a17f47b | |||
| 3fc2a763b4 | |||
| 1b2726a55c | |||
| 229fa27b73 | |||
| 072f44245d | |||
| dc837c430f | |||
| 429e270a9e | |||
| 985cefc297 | |||
| 285fc0ced3 | |||
| 777e14b6d4 | |||
| d18e8211ca | |||
| 2984980a54 | |||
| 2302f31f18 | |||
| f673677c2a | |||
| acc9bf77fb | |||
| 678769da0e | |||
| 2389208c95 | |||
| 28a1080ccc | |||
| 432d3fd3c3 | |||
| 8fc73b866b | |||
| dbc62aac93 | |||
| 79802a46fd | |||
| 3d8ecb42c1 | |||
| bcaf554ae4 | |||
| 3ac4a921e0 | |||
| 128f86b643 | |||
| 828fc690b6 | |||
| 55d06640b5 | |||
| 3874a8da21 | |||
| 6a3ecd5b09 | |||
| afcd4f7262 | |||
| b0ae731fa6 | |||
| 0b678fe69a | |||
| 4cecc78a74 | |||
| 92273d2fc6 | |||
| 04ec749c3c | |||
| 165377816c | |||
| 729904e1c3 | |||
| 6bc4fa0a82 | |||
| f12403672b | |||
| 97daff8d4f | |||
| a32451fd7f | |||
| ca14770971 | |||
| 35ac0695c7 | |||
| 589b614a53 | |||
| 64ea3e05d8 | |||
| 5d0e115438 | |||
| 9f35c1eded | |||
| ff07346fe9 | |||
| 22fffc2f8c | |||
| 205363dd42 | |||
| d2297b882f | |||
| 48bf8dbc5b | |||
| 9585c760d5 | |||
| 8c2cd0aa4d | |||
| 2e680b04c9 | |||
| e2b81f7b57 | |||
| c38b277887 | |||
| a1f6304b22 | |||
| 09d7f56efc | |||
| 9221f3fc96 | |||
| 6b30a0aebc | |||
| 158563f387 | |||
| b0fc5752e2 | |||
| 28903615ed | |||
| caa83c0432 | |||
| 045f4a42db | |||
| 3275bc9cae | |||
| 6d14eaefe1 | |||
| 03274725be | |||
| 0951006063 | |||
| 616bfc6a2a | |||
| c0bfe429ee | |||
| c1d90485a1 | |||
| 6c30527684 | |||
| 4ac751f492 | |||
| de7b137257 | |||
| 0bf689fa8d | |||
| f31ef1649f | |||
| c66bc62c41 | |||
| 9ee59215c8 | |||
| a991adecaf | |||
| 601b1d5c89 | |||
| 8b593883a7 | |||
| bb549c9a89 | |||
| 9f2005622a | |||
| 4238bde13a | |||
| 2d4ce1aac0 | |||
| 37269abde2 | |||
| 54038eabd4 | |||
| 57922e3071 | |||
| ad36dfd448 | |||
| 6e334d2efb | |||
| d3dc1401fd | |||
| 0f59c9911d | |||
| 0ebfd2bc76 | |||
| 734625c1e3 | |||
| eaaea8a64f | |||
| c026ab1777 | |||
| 2c7193efea | |||
| 21f5ef469b | |||
| e7f9eb6e06 | |||
| 9db9c382ab | |||
| b2ba626cde | |||
| 85d93c4f4b | |||
| 09f6dd8d82 | |||
| 2aa6df48c6 | |||
| 2a16260b05 | |||
| c28158b041 | |||
| 661199850c | |||
| 67b69a45cc | |||
| 579322578b | |||
| 0de65042b0 | |||
| f9643f8651 | |||
| fc9ac4c40e | |||
| 84843066f2 | |||
| af3d5c4654 | |||
| 676f3daf50 | |||
| 54970015cb | |||
| 2b49430530 | |||
| feea5f3518 | |||
| 04e580b40f | |||
| f07a3ba97d | |||
| f44e3a81ab | |||
| 77d3bd019e | |||
| 72725d5ab4 | |||
| aa717ed1fe | |||
| 292b49ba79 | |||
| c600eb5d5a | |||
| 6d2f788fa2 | |||
| b8acff3e7c | |||
| b8bdb03fc0 | |||
| 7257abefb4 | |||
| 3095613a76 | |||
| db12b64b3a | |||
| a081edde25 | |||
| 5496c4a10a | |||
| c968ded99a | |||
| 4224a833b4 | |||
| df470f1a5e | |||
| ed0100a82c | |||
| 0ad72e8334 | |||
| 50ee5d1f49 | |||
| 94283a8da2 | |||
| 86e9a3e838 | |||
| d77b9ef7c9 | |||
| e29afa3155 | |||
| 7376fbe7a1 | |||
| 04e98e1c39 | |||
| ddca6e7ec9 | |||
| 1ac12ac668 | |||
| 399936e3ab | |||
| 3d086992dc | |||
| 62ded1290c | |||
| 33cbdfbd13 | |||
| f5ce6ed4bc | |||
| eb9a2ac2e0 | |||
| 8bed529d05 | |||
| 41a8b8007a | |||
| 141dc843f3 | |||
| 902825404e | |||
| 749eaaab30 | |||
| 8f5767b992 | |||
| 715f0c5853 | |||
| a960fd3d56 | |||
| 17b8ac6d0b | |||
| 4ac78fe4d1 | |||
| 957bcf790f | |||
| 66f0b38008 | |||
| f6a2246aab | |||
| fa3e941069 | |||
| 9bb049f746 | |||
| c41f2a4d65 | |||
| 06ff9f2499 | |||
| f91f9fcc94 | |||
| 9a626b0d4f | |||
| ac2adaf4ba | |||
| 3ab198615b | |||
| 7c2831098c | |||
| 2454e22ea2 | |||
| b1fb40ca61 | |||
| e403e938c3 | |||
| 952ba1f1ea | |||
| a25690c2d3 | |||
| 6b6b9c61d7 | |||
| fcc5e522a7 | |||
| c33c0487cf | |||
| 1753a6c247 | |||
| 195f513416 | |||
| c6d38bb3d7 | |||
| 9b2fba9600 | |||
| c88f6501fa | |||
| c511346160 | |||
| 0b02daac1d | |||
| 2390c085e4 | |||
| 698d94feed | |||
| a9a6d39127 | |||
| 395bd31898 | |||
| 7e24492ce8 | |||
| 19d3d80013 | |||
| 8c7875d7ea | |||
| 110ce0d4c6 | |||
| ff8c57fdab | |||
| 54ccdc57bf | |||
| 2463f06ba1 | |||
| 88d5b1f98f | |||
| 43522e9c81 | |||
| 6e798f739f | |||
| 5b5f1280af | |||
| 2188e8dd78 | |||
| 8659e9ea37 | |||
| 4cff481d61 | |||
| 6da910d8fb | |||
| cc08ad46e3 | |||
| 5b52c1adbb | |||
| a20958a89b | |||
| dea36d4b80 | |||
| 6cb7e4caf7 | |||
| 6e41668b25 | |||
| d3e1acddc5 | |||
| a9c511eb2e | |||
| ffef33a9fc | |||
| 982917ddbb | |||
| 07406a50bb | |||
| 78f325e127 | |||
| e1bb97a1db | |||
| 831952806d | |||
| 4c57b7a009 | |||
| 683188f67a | |||
| 848e5271ab | |||
| 7ca5614c44 | |||
| 70fc781a03 | |||
| aafdbab781 | |||
| 5dd0f7ea10 | |||
| 9393d9105c | |||
| 2c3856be3c | |||
| dc746a51a5 | |||
| 0f2c9354f0 | |||
| af00032ee9 | |||
| 5d7e685dc4 | |||
| bfcb79c02b | |||
| ebee6273b0 | |||
| b45900e5bc | |||
| 9f0657683a | |||
| 35b8d8ca25 | |||
| 8f7095ce19 | |||
| d9c8dd20e9 | |||
| 8dcc462a30 | |||
| b561948030 | |||
| 2cbd8684cf | |||
| 107c9fce94 | |||
| 4c18b3e059 | |||
| 9960ec4d58 | |||
| 2a09e048e4 | |||
| 9711f3ba72 | |||
| 0622d2b81b | |||
| a372d1fb60 | |||
| 99e55e730e | |||
| 57d1e915e6 | |||
| 96ad6228bd | |||
| 678f9fbe87 | |||
| 714933df56 | |||
| f0ef9ad51e | |||
| 0255213934 | |||
| 0ad92a999c | |||
| b06456d573 | |||
| 6f1fc2c9b4 | |||
| 44a1982d87 | |||
| 6b0cf5aa96 | |||
| 2cfac7a772 | |||
| 99aaf35e0b | |||
| 942ed1fb55 | |||
| 25f83a98e3 | |||
| ed4040f2ec | |||
| 41034de676 | |||
| 2db0f1a6c8 | |||
| 6b06cc7ef5 | |||
| 2bf26212af | |||
| 9d273c172d | |||
| 6ae3bc82bb | |||
| c782002274 | |||
| cd0906d041 | |||
| f936ecca33 | |||
| 8794e3bc53 | |||
| 4503e2a222 | |||
| 015725f88c | |||
| 39451f0e37 | |||
| 2bb0b7faa5 | |||
| 8db7c3769a | |||
| d4e32de882 | |||
| db75dea9ee | |||
| 489bba9c4b | |||
| 4accc49d60 | |||
| 5a47cd5216 | |||
| 439608cf27 | |||
| f9df7a1b5a | |||
| 78c801994a | |||
| 882fd68cf9 | |||
| 3433b73edf | |||
| a235d6a8cd | |||
| d76e88af21 | |||
| 3f6c6c443a | |||
| 13c6d406cf | |||
| 19fea4e761 | |||
| c84c96dcd1 | |||
| cdd8ccc2d4 | |||
| 09210d5d40 | |||
| 8f44a26037 | |||
| 5e986b2d04 | |||
| 298c0922cb | |||
| dc127ea6a3 | |||
| 522ed94c32 | |||
| 6edf66a599 | |||
| 475ec24528 | |||
| b555af0df7 | |||
| f90430e544 | |||
| 4ccb75818c | |||
| c5c9ed24c3 | |||
| 386aa0adc1 | |||
| 0b26b75699 | |||
| d013f67c70 | |||
| dc7c0e61fd | |||
| 89bd041d29 | |||
| ac730d6086 | |||
| 427eaed544 | |||
| 00f783c0b6 | |||
| 8e1e53d55e | |||
| 5c9a5c13b8 | |||
| c1f03a8e75 | |||
| 1affa83620 | |||
| 2aa8180113 | |||
| e6001d83a4 | |||
| 8550c8fde9 | |||
| 48d9a09307 | |||
| 9d0b874488 | |||
| 062a13b2c2 | |||
| 937f8723ed | |||
| 0c5cabbd79 | |||
| 95d8f710d8 | |||
| bb9b8b34e5 | |||
| 54c1164bd7 | |||
| 89c1158d95 | |||
| 9cae189941 | |||
| 53a31cd4c4 | |||
| d72a615481 | |||
| 8dcffa80a8 | |||
| 7cf53acd18 | |||
| 748cf68055 | |||
| 9cd22bdc06 | |||
| 5830d4b91c | |||
| b42f7f566b | |||
| db8223ca98 | |||
| 9403afc392 | |||
| 2b2d3b9517 | |||
| 3eebdfcdb3 | |||
| f7766bc3d4 | |||
| f2614abbdd | |||
| 58824ea879 | |||
| 9ef7a18847 | |||
| 58c0167696 | |||
| 9adfd286f9 | |||
| 4e8f530fbb | |||
| 3e694b0772 | |||
| 2fd5b04d4d | |||
| 4688b9a9c9 | |||
| 153e0035ba | |||
| 88b6ecc557 | |||
| 9df55874a6 | |||
| 87d4ab827a | |||
| 318166f23a | |||
| cc3f712659 | |||
| ee399d8a08 | |||
| 96c233d5c5 | |||
| 652b3e1954 | |||
| a8b76d5537 | |||
| e041b70cdd | |||
| 63bf912b3e | |||
| 0cbe7b1655 | |||
| 7bbec29c5b | |||
| 7cec7dbac8 | |||
| 09a19b5f42 | |||
| 8fe765c097 | |||
| b11e8e07c4 | |||
| f72763306d | |||
| e3d1a476e2 | |||
| f0bc86d42f | |||
| a62e806175 | |||
| 5bcf5bf93e | |||
| 4f35ba0931 | |||
| 2bcdfe778a | |||
| c89da1d0f7 | |||
| 46a1eda029 | |||
| 1c39819112 | |||
| 0efe617c03 | |||
| 10df947efe | |||
| fb7790ba4a | |||
| a9338ed822 | |||
| eaab8cdd93 | |||
| edfcd0dc6e | |||
| 838b56089b | |||
| 178810f908 | |||
| 69f5aca853 | |||
| 8e6aece9ae | |||
| 59b883ff7f | |||
| 548d34fbf4 | |||
| bb6345ccfa | |||
| d59a10f718 | |||
| aab1c5419a | |||
| 9b83785b95 | |||
| 099b710eb1 | |||
| a5424afc38 | |||
| 626325066d | |||
| 37195f6008 | |||
| fcbc68cefe | |||
| 9241953e31 | |||
| 651a912498 | |||
| a05f6fb6b5 | |||
| d6440d31f2 | |||
| 88d5dc2f17 | |||
| a17ad85858 | |||
| 4b49c1f30f | |||
| a9e36b9a59 | |||
| 80429bbfb8 | |||
| f39e20d7a7 | |||
| a03bac5d74 | |||
| 1aff09598a | |||
| 4036a71ee1 | |||
| a966be2f46 | |||
| a0b3bc1cab | |||
| eb2e2b0a26 | |||
| 55ad7b2e81 | |||
| dbcd2897a4 | |||
| 68a6d1c166 | |||
| fe82ec6fc2 | |||
| 5f2819a961 | |||
| 812bfc7cf5 | |||
| d164cafd33 | |||
| 3ada4183d9 | |||
| fa68621b41 | |||
| 4f2b9d39da | |||
| fd01c9269f | |||
| 82d150e53a | |||
| 251f3fe2da | |||
| 4f0e1e6b3d | |||
| a5dbf5d4b7 | |||
| 38baf77c30 | |||
| bfb8b03fc9 | |||
| 2dd38d9b03 | |||
| 307c64bc1e | |||
| 782e3a85f9 | |||
| 3bae6e749a | |||
| 530ef6b83e | |||
| 5b334eb2d5 | |||
| 28dc2e425a | |||
| 83b72e7403 | |||
| 01e1f65ffe | |||
| 9b15f888e6 | |||
| 171b8afa8e | |||
| 27f2f9f13a | |||
| 2c3983cead | |||
| bebd043d58 | |||
| a1c828fe62 | |||
| dfb885f38d | |||
| 702c095544 | |||
| b3e886d444 | |||
| 46d85e92cd | |||
| 0d84f2857f | |||
| ac6c80db90 | |||
| e608e85d56 | |||
| 58104a9a4d | |||
| 8d68dcabb5 | |||
| a5e5389d6c | |||
| 8de862c82f | |||
| 5141303ee1 | |||
| 3d95680cbc | |||
| 78663b873c | |||
| 0a106026dd | |||
| 999ca6274c | |||
| 36b9177069 | |||
| 8cf7bf859b | |||
| 2e54b62f60 | |||
| df46069d92 | |||
| e31014dde4 | |||
| f9a14581e1 | |||
| 95ec005894 | |||
| 73d271b8bc | |||
| 0c3b56e44a | |||
| 736f340979 | |||
| 49e62d35c3 | |||
| 810bce7495 | |||
| aff876aa05 | |||
| 9511644ce6 | |||
| 21d73e5f69 | |||
| d62f3fb936 | |||
| 0a011b6075 | |||
| 4e561dc764 | |||
| a55256ad82 | |||
| ab26ef64a5 | |||
| 6d1610eee0 | |||
| 2ba143d6ea | |||
| bd542ac308 | |||
| e71ffd1a77 | |||
| 1ac968f63c | |||
| dc4f62a085 | |||
| ad91d4f1ce | |||
| 8d4c7512ab | |||
| 12d5837526 | |||
| c82d2abab4 | |||
| 26cb717a08 | |||
| 0b1cc0ef5b | |||
| 05dc9138b4 | |||
| a7dcacb26c | |||
| 3ac3f871e4 | |||
| b180c0bbe6 | |||
| e4987f3bde | |||
| ee2690c2cc | |||
| 4cad26f793 | |||
| 84f3d5fec5 | |||
| bb3939d570 | |||
| b303f708f5 | |||
| d39550e090 | |||
| 22d956f38a | |||
| dab7728138 | |||
| 4038b6bc51 | |||
| bcaf3a246c | |||
| 4fb115fcbc | |||
| bfd4b2b6de | |||
| c0cd3fc5c2 | |||
| f95c4393d2 | |||
| e2eb5fabcc | |||
| 7275e8ff0d | |||
| accfc3df12 | |||
| 35392483d9 | |||
| 9770851fd4 | |||
| e013a6f087 | |||
| 1ec9ff20b1 | |||
| 9b0dea80c9 | |||
| eea1ea7ed0 | |||
| 85cd46bfc7 | |||
| a7ca394864 | |||
| e178a0795a | |||
| e8b0470ceb | |||
| c08abfdfdf | |||
| b1c765eb51 | |||
| 4b0f7d45e8 | |||
| 9e6271b1dc | |||
| a96eb31dc9 | |||
| 221a7809b6 | |||
| 8c33243c90 | |||
| c4b07b98aa | |||
| 1fda80a86b | |||
| 1287c729f2 | |||
| c069faa6f4 | |||
| 286fd91b2b | |||
| 33250d2f3d | |||
| 44ca940ca3 | |||
| 3b0ef7a96d | |||
| dfb6c593e4 | |||
| 853b01e2ca | |||
| 5a924fa382 | |||
| d4985a024d | |||
| 2797266de6 | |||
| b476cc91ca | |||
| 9e3aa19a09 | |||
| 7443e8a532 | |||
| 1d6dbf63c0 | |||
| e17687f80d | |||
| 27f4e14a4e | |||
| 8d5de98218 | |||
| dbf5c0a5bd | |||
| c1422f789a | |||
| 613af9399a | |||
| 34a6108bb2 | |||
| 66a5508abe | |||
| 3a273ea64f | |||
| 6f88e6ef26 | |||
| fd7905833e | |||
| 1977e436d6 | |||
| 0dfb3d00e9 | |||
| 785ec9bdb1 | |||
| eeccc4fd49 | |||
| efea9a7c37 | |||
| 04eafd6705 | |||
| 73b554aa48 | |||
| d3ddcf4c20 | |||
| ed9ea2f1d3 | |||
| 7eae3e9923 | |||
| 03d95033d7 | |||
| 9a79606565 | |||
| c5a101aad2 | |||
| 2dbf4d652d | |||
| 7364380312 | |||
| b7fe70aba3 | |||
| bca9982c57 | |||
| 7c16435010 | |||
| a79def625c | |||
| 8d5f804a60 | |||
| 0167381f0d | |||
| 4e2c4b39bb | |||
| 7bfc84abc8 | |||
| 5ac6c64079 | |||
| e7c4261b86 | |||
| 163b75e81b | |||
| 949132ef5a | |||
| 5617d31ed8 | |||
| 832d865397 |
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -9,35 +9,44 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout
|
||||||
name: Checkout
|
uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
|
||||||
-
|
- name: Docker meta
|
||||||
name: Set up docker tag
|
id: meta
|
||||||
id: vars
|
uses: docker/metadata-action@v4
|
||||||
run: echo ::set-output name=RELEASE_TAG::${GITHUB_REF/refs\/tags\/v/}
|
with:
|
||||||
-
|
images: |
|
||||||
name: Set up QEMU
|
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
||||||
uses: docker/setup-qemu-action@v1
|
tags: |
|
||||||
-
|
type=semver,pattern={{version}}
|
||||||
name: Set up Docker Buildx
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
uses: docker/setup-buildx-action@v1
|
type=raw,value=latest
|
||||||
-
|
|
||||||
name: Login to DockerHub
|
- name: Set up QEMU
|
||||||
uses: docker/login-action@v1
|
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:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
-
|
|
||||||
name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
context: .
|
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
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
RELEASE_BUILD=1
|
RELEASE_BUILD=1
|
||||||
tags: |
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:${{ steps.vars.outputs.RELEASE_TAG }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:latest
|
|
||||||
@@ -9,33 +9,41 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout
|
||||||
name: Checkout
|
uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
|
||||||
-
|
- name: Docker meta
|
||||||
name: Set up docker tag
|
id: meta
|
||||||
id: vars
|
uses: docker/metadata-action@v4
|
||||||
run: echo ::set-output name=BUILD_DATE::$(date '+%Y%m%d')
|
with:
|
||||||
-
|
images: |
|
||||||
name: Set up QEMU
|
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
||||||
uses: docker/setup-qemu-action@v1
|
tags: |
|
||||||
-
|
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
|
||||||
name: Set up Docker Buildx
|
type=raw,value=latest-snapshot
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
-
|
- name: Set up QEMU
|
||||||
name: Login to DockerHub
|
uses: docker/setup-qemu-action@v2
|
||||||
uses: docker/login-action@v1
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
-
|
|
||||||
name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
context: .
|
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
|
push: true
|
||||||
tags: |
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:SNAPSHOT-${{ steps.vars.outputs.BUILD_DATE }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:latest-snapshot
|
|
||||||
@@ -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
@@ -1,5 +1,5 @@
|
|||||||
# Build backend binary file
|
# 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
|
ARG RELEASE_BUILD
|
||||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||||
@@ -9,7 +9,7 @@ RUN apk add git gcc g++ libc-dev
|
|||||||
RUN ./build.sh backend
|
RUN ./build.sh backend
|
||||||
|
|
||||||
# Build frontend files
|
# 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
|
ARG RELEASE_BUILD
|
||||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||||
@@ -19,7 +19,7 @@ RUN apk add git
|
|||||||
RUN ./build.sh frontend
|
RUN ./build.sh frontend
|
||||||
|
|
||||||
# Package docker image
|
# Package docker image
|
||||||
FROM alpine:3.13.5
|
FROM alpine:3.20.1
|
||||||
LABEL maintainer="MaysWind <i@mayswind.net>"
|
LABEL maintainer="MaysWind <i@mayswind.net>"
|
||||||
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
|
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
|
||||||
RUN apk --no-cache add tzdata
|
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 chmod +x /docker-entrypoint.sh
|
||||||
RUN mkdir -p /ezbookkeeping && chown 1000:1000 /ezbookkeeping \
|
RUN mkdir -p /ezbookkeeping && chown 1000:1000 /ezbookkeeping \
|
||||||
&& mkdir -p /ezbookkeeping/data && chown 1000:1000 /ezbookkeeping/data \
|
&& 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
|
WORKDIR /ezbookkeeping
|
||||||
COPY --from=be-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/ezbookkeeping /ezbookkeeping/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 --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 conf /ezbookkeeping/conf
|
||||||
|
COPY --chown=1000:1000 templates /ezbookkeeping/templates
|
||||||
COPY --chown=1000:1000 LICENSE /ezbookkeeping/LICENSE
|
COPY --chown=1000:1000 LICENSE /ezbookkeeping/LICENSE
|
||||||
USER 1000:1000
|
USER 1000:1000
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ezBookkeeping
|
# ezBookkeeping
|
||||||
[](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
[](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
||||||
[](https://github.com/mayswind/ezbookkeeping/actions)
|
[](https://github.com/mayswind/ezbookkeeping/actions)
|
||||||
[](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
|
[](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
|
||||||
[](https://hub.docker.com/r/mayswind/ezbookkeeping)
|
[](https://hub.docker.com/r/mayswind/ezbookkeeping)
|
||||||
[](https://github.com/mayswind/ezbookkeeping/releases)
|
[](https://github.com/mayswind/ezbookkeeping/releases)
|
||||||
@@ -8,17 +8,21 @@
|
|||||||
## Introduction
|
## 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.
|
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
|
## Features
|
||||||
1. Open source & Self-hosted
|
1. Open source & Self-hosted
|
||||||
2. Lightweight & Fast
|
2. Lightweight & Fast
|
||||||
3. Easy to install
|
3. Easy to install
|
||||||
* Docker support
|
* Docker support
|
||||||
* Multiple database support (sqlite, mysql, etc.)
|
* Multiple database support (SQLite, MySQL, PostgreSQL, etc.)
|
||||||
* Multiple os & architecture support (Windows, macOS, Linux & x86, amd64, ARM)
|
* Multiple operation system & hardware support (Windows, macOS, Linux & x86, amd64, ARM)
|
||||||
4. User-friendly interface
|
4. User-friendly interface
|
||||||
|
* Both desktop and mobile UI
|
||||||
* Close to native app experience (for mobile device)
|
* Close to native app experience (for mobile device)
|
||||||
* Two-level account & two-level category support
|
* Two-level account & two-level category support
|
||||||
* Plentiful preset categories
|
* Plentiful preset categories
|
||||||
|
* Geographic location and map support
|
||||||
* Searching & filtering history records
|
* Searching & filtering history records
|
||||||
* Data statistics
|
* Data statistics
|
||||||
* Dark theme
|
* Dark theme
|
||||||
@@ -26,12 +30,15 @@ ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It c
|
|||||||
6. Multiple timezone support
|
6. Multiple timezone support
|
||||||
7. Multi-language support
|
7. Multi-language support
|
||||||
8. Two-factor authentication
|
8. Two-factor authentication
|
||||||
9. Application lock (WebAuthn support)
|
9. Application lock (PIN code / WebAuthn)
|
||||||
10. Data export
|
10. Data export
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
### Mobile Device
|
### Desktop Version
|
||||||
[](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/en.png)
|
[](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/desktop/en.png)
|
||||||
|
|
||||||
|
### Mobile Version
|
||||||
|
[](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/en.png)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
### Ship with docker
|
### Ship with docker
|
||||||
@@ -48,19 +55,35 @@ Latest Daily Build:
|
|||||||
### Install from binary
|
### Install from binary
|
||||||
Latest release: [https://github.com/mayswind/ezbookkeeping/releases](https://github.com/mayswind/ezbookkeeping/releases)
|
Latest release: [https://github.com/mayswind/ezbookkeeping/releases](https://github.com/mayswind/ezbookkeeping/releases)
|
||||||
|
|
||||||
|
**Linux / macOS**
|
||||||
|
|
||||||
$ ./ezbookkeeping server run
|
$ ./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
|
### 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:
|
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
|
$ ./build.sh package -o ezbookkeeping.tar.gz
|
||||||
|
|
||||||
All the files will be packaged in `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:
|
You can also build docker image, make sure you have [docker](https://www.docker.com/) installed, then follow these steps:
|
||||||
|
|
||||||
|
**Linux**
|
||||||
|
|
||||||
$ ./build.sh docker
|
$ ./build.sh docker
|
||||||
|
|
||||||
## Documents
|
## Documents
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
TYPE=""
|
TYPE=""
|
||||||
|
NO_LINT="0"
|
||||||
|
NO_TEST="0"
|
||||||
RELEASE=${RELEASE_BUILD:-"0"}
|
RELEASE=${RELEASE_BUILD:-"0"}
|
||||||
RELEASE_TYPE="unknown"
|
RELEASE_TYPE="unknown"
|
||||||
VERSION=""
|
VERSION=""
|
||||||
@@ -31,16 +33,18 @@ Usage:
|
|||||||
build.sh type [options]
|
build.sh type [options]
|
||||||
|
|
||||||
Types:
|
Types:
|
||||||
backend Build backend binary file
|
backend Build backend binary file
|
||||||
frontend Build frontend files
|
frontend Build frontend files
|
||||||
package Build package archive
|
package Build package archive
|
||||||
docker Build docker image
|
docker Build docker image
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
|
-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)
|
-o, --output <filename> Package file name (For "package" type only)
|
||||||
-t, --tag Docker tag (For "docker" type only)
|
-t, --tag Docker tag (For "docker" type only)
|
||||||
-h, --help Show help
|
--no-lint Do not execute lint check before building
|
||||||
|
--no-test Do not execute unit testing before building
|
||||||
|
-h, --help Show help
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +67,12 @@ parse_args() {
|
|||||||
DOCKER_TAG="$2"
|
DOCKER_TAG="$2"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--no-lint)
|
||||||
|
NO_LINT="1"
|
||||||
|
;;
|
||||||
|
--no-test)
|
||||||
|
NO_TEST="1"
|
||||||
|
;;
|
||||||
--help | -h)
|
--help | -h)
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
@@ -111,6 +121,30 @@ set_build_parameters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
build_backend() {
|
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="-X main.Version=$VERSION"
|
||||||
backend_build_extra_arguments="$backend_build_extra_arguments -X main.CommitHash=$COMMIT_HASH"
|
backend_build_extra_arguments="$backend_build_extra_arguments -X main.CommitHash=$COMMIT_HASH"
|
||||||
|
|
||||||
@@ -125,17 +159,26 @@ build_backend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
build_frontend() {
|
build_frontend() {
|
||||||
frontend_build_arguments="";
|
|
||||||
|
|
||||||
if [ "$RELEASE" = "0" ]; then
|
|
||||||
frontend_build_arguments="--buildUnixTime=$BUILD_UNIXTIME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Pulling frontend dependencies..."
|
echo "Pulling frontend dependencies..."
|
||||||
npm install
|
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)..."
|
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() {
|
build_package() {
|
||||||
@@ -158,9 +201,13 @@ build_package() {
|
|||||||
|
|
||||||
rm -rf package
|
rm -rf package
|
||||||
mkdir package
|
mkdir package
|
||||||
|
mkdir package/data
|
||||||
|
mkdir package/storage
|
||||||
|
mkdir package/log
|
||||||
cp ezbookkeeping package/
|
cp ezbookkeeping package/
|
||||||
cp -R dist package/public
|
cp -R dist package/public
|
||||||
cp -R conf package/conf
|
cp -R conf package/conf
|
||||||
|
cp -R templates package/templates
|
||||||
cp LICENSE package/
|
cp LICENSE package/
|
||||||
|
|
||||||
cd package || { echo_red "Error: Build Failed"; exit 1; }
|
cd package || { echo_red "Error: Build Failed"; exit 1; }
|
||||||
|
|||||||
+10
-2
@@ -58,7 +58,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
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))
|
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactorRecoveryCode))
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
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))
|
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")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-11
@@ -7,9 +7,12 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||||
)
|
)
|
||||||
@@ -17,64 +20,117 @@ import (
|
|||||||
func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
||||||
var err error
|
var err error
|
||||||
configFilePath := c.String("conf-path")
|
configFilePath := c.String("conf-path")
|
||||||
|
isDisableBootLog := c.Bool("no-boot-log")
|
||||||
|
|
||||||
if configFilePath != "" {
|
if configFilePath != "" {
|
||||||
if _, err = os.Stat(configFilePath); err != nil {
|
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
|
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 {
|
} else {
|
||||||
configFilePath, err = settings.GetDefaultConfigFilePath()
|
configFilePath, err = settings.GetDefaultConfigFilePath()
|
||||||
|
|
||||||
if err != nil {
|
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
|
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)
|
config, err := settings.LoadConfiguration(configFilePath)
|
||||||
|
|
||||||
if err != nil {
|
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
|
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)
|
settings.SetCurrentConfig(config)
|
||||||
|
|
||||||
err = datastore.InitializeDataStore(config)
|
err = datastore.InitializeDataStore(config)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = log.SetLoggerConfiguration(config)
|
err = log.SetLoggerConfiguration(config, isDisableBootLog)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = uuid.InitializeUuidGenerator(config)
|
err = uuid.InitializeUuidGenerator(config)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = exchangerates.InitializeExchangeRatesDataSource(config)
|
err = exchangerates.InitializeExchangeRatesDataSource(config)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgJson, _ := json.Marshal(getConfigWithoutSensitiveData(config))
|
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
|
return config, nil
|
||||||
}
|
}
|
||||||
@@ -88,7 +144,10 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clonedConfig.DatabaseConfig.DatabasePassword = "****"
|
clonedConfig.DatabaseConfig.DatabasePassword = "****"
|
||||||
|
clonedConfig.SMTPConfig.SMTPPasswd = "****"
|
||||||
|
clonedConfig.MinIOConfig.SecretAccessKey = "****"
|
||||||
clonedConfig.SecretKey = "****"
|
clonedConfig.SecretKey = "****"
|
||||||
|
clonedConfig.AmapApplicationSecret = "****"
|
||||||
|
|
||||||
return clonedConfig
|
return clonedConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -2,13 +2,14 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
clis "github.com/mayswind/ezbookkeeping/pkg/cli"
|
clis "github.com/mayswind/ezbookkeeping/pkg/cli"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"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",
|
Name: "user-delete",
|
||||||
Usage: "Delete specified user",
|
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",
|
Name: "transaction-check",
|
||||||
Usage: "Check whether user all transactions and accounts are correct",
|
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",
|
Name: "transaction-export",
|
||||||
Usage: "Export user all transactions to csv file",
|
Usage: "Export user all transactions to file",
|
||||||
Action: exportUserTransaction,
|
Action: exportUserTransaction,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
@@ -168,6 +260,12 @@ var UserData = &cli.Command{
|
|||||||
Required: true,
|
Required: true,
|
||||||
Usage: "Specific exported file path (e.g. transaction.csv)",
|
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
|
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 {
|
func deleteUser(c *cli.Context) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
@@ -270,11 +488,11 @@ func disableUser2FA(c *cli.Context) error {
|
|||||||
err = clis.UserData.DisableUserTwoFactorAuthorization(c, username)
|
err = clis.UserData.DisableUserTwoFactorAuthorization(c, username)
|
||||||
|
|
||||||
if err != nil {
|
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
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -348,6 +566,29 @@ func checkUserTransactionAndAccount(c *cli.Context) error {
|
|||||||
return nil
|
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 {
|
func exportUserTransaction(c *cli.Context) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
@@ -357,9 +598,15 @@ func exportUserTransaction(c *cli.Context) error {
|
|||||||
|
|
||||||
username := c.String("username")
|
username := c.String("username")
|
||||||
filePath := c.String("file")
|
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 == "" {
|
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
|
return os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +619,7 @@ func exportUserTransaction(c *cli.Context) error {
|
|||||||
|
|
||||||
log.BootInfof("[user_data.exportUserTransaction] starting exporting user \"%s\" data", username)
|
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 {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.exportUserTransaction] error occurs when exporting user data")
|
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("[Nickname] %s\n", user.Nickname)
|
||||||
fmt.Printf("[Password] %s\n", user.Password)
|
fmt.Printf("[Password] %s\n", user.Password)
|
||||||
fmt.Printf("[Salt] %s\n", user.Salt)
|
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("[DefaultCurrency] %s\n", user.DefaultCurrency)
|
||||||
fmt.Printf("[FirstDayOfWeek] %s\n", user.FirstDayOfWeek)
|
fmt.Printf("[FirstDayOfWeek] %s (%d)\n", user.FirstDayOfWeek, user.FirstDayOfWeek)
|
||||||
fmt.Printf("[TransactionEditScope] %s\n", user.TransactionEditScope)
|
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("[Deleted] %t\n", user.Deleted)
|
||||||
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
|
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
|
||||||
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
|
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) {
|
func printTokenInfo(token *models.TokenRecord) {
|
||||||
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.CreatedUnixTime), token.CreatedUnixTime)
|
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("[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)
|
fmt.Printf("[UserAgent] %s\n", token.UserAgent)
|
||||||
}
|
}
|
||||||
|
|||||||
+129
@@ -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
@@ -3,7 +3,10 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cache"
|
||||||
|
"github.com/gin-contrib/cache/persistence"
|
||||||
"github.com/gin-contrib/gzip"
|
"github.com/gin-contrib/gzip"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
@@ -41,13 +44,13 @@ func startWebServer(c *cli.Context) error {
|
|||||||
return err
|
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 {
|
if config.AutoUpdateDatabase {
|
||||||
err = updateAllDatabaseTablesStructure()
|
err = updateAllDatabaseTablesStructure()
|
||||||
|
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +58,7 @@ func startWebServer(c *cli.Context) error {
|
|||||||
err = requestid.InitializeRequestIdGenerator(config)
|
err = requestid.InitializeRequestIdGenerator(config)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +68,14 @@ func startWebServer(c *cli.Context) error {
|
|||||||
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
|
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 {
|
if config.Mode == settings.MODE_PRODUCTION {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workboxFileNames := utils.ListFileNamesWithPrefixAndSuffix(config.StaticRootPath, "workbox-", ".js")
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(bindMiddleware(middlewares.Recovery))
|
router.Use(bindMiddleware(middlewares.Recovery))
|
||||||
|
|
||||||
@@ -84,6 +89,7 @@ func startWebServer(c *cli.Context) error {
|
|||||||
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
|
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
|
||||||
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
|
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
|
||||||
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
|
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
|
||||||
|
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
router.NoRoute(bindApi(api.Default.ApiNotFound))
|
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/css", filepath.Join(config.StaticRootPath, "css"))
|
||||||
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
|
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
|
||||||
router.Static("/mobile/fonts", filepath.Join(config.StaticRootPath, "fonts"))
|
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.ico", filepath.Join(config.StaticRootPath, "favicon.ico"))
|
||||||
router.StaticFile("/mobile/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
|
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/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
||||||
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
||||||
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
|
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 := router.Group("/desktop")
|
||||||
desktopEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
|
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/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
|
||||||
router.StaticFile("/desktop/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.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/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 := router.Group("/api")
|
||||||
|
|
||||||
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
|
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
|
||||||
apiRoute.Use(bindMiddleware(middlewares.RequestLog))
|
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 {
|
if config.EnableTwoFactor {
|
||||||
twoFactorRoute := apiRoute.Group("/2fa")
|
twoFactorRoute := apiRoute.Group("/2fa")
|
||||||
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization))
|
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization))
|
||||||
{
|
{
|
||||||
twoFactorRoute.POST("/authorize.json", bindApi(api.Authorizations.TwoFactorAuthorizeHandler))
|
twoFactorRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeHandler, config))
|
||||||
twoFactorRoute.POST("/recovery.json", bindApi(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler))
|
twoFactorRoute.POST("/recovery.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler, config))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.EnableUserRegister {
|
if config.EnableUserRegister {
|
||||||
apiRoute.POST("/register.json", bindApi(api.Users.UserRegisterHandler))
|
apiRoute.POST("/register.json", bindApiWithTokenUpdate(api.Users.UserRegisterHandler, config))
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.EnableDataExport {
|
if config.EnableUserVerifyEmail {
|
||||||
dataRoute := apiRoute.Group("/data")
|
apiRoute.POST("/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByUnloginUserHandler))
|
||||||
dataRoute.Use(bindMiddleware(middlewares.HeaderInQueryString))
|
|
||||||
dataRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
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 := apiRoute.Group("/v1")
|
||||||
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization))
|
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.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
|
||||||
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
|
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
|
||||||
apiV1Route.POST("/tokens/revoke_all.json", bindApi(api.Tokens.TokenRevokeAllHandler))
|
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
|
// Users
|
||||||
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
|
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 {
|
if config.EnableTwoFactor {
|
||||||
apiV1Route.GET("/users/2fa/status.json", bindApi(api.TwoFactorAuthorizations.TwoFactorStatusHandler))
|
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/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/disable.json", bindApi(api.TwoFactorAuthorizations.TwoFactorDisableHandler))
|
||||||
apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler))
|
apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
|
apiV1Route.GET("/data/statistics.json", bindApi(api.DataManagements.DataStatisticsHandler))
|
||||||
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
|
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
|
// Accounts
|
||||||
apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler))
|
apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler))
|
||||||
apiV1Route.GET("/accounts/get.json", bindApi(api.Accounts.AccountGetHandler))
|
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.json", bindApi(api.Transactions.TransactionListHandler))
|
||||||
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
|
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
|
||||||
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
|
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.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.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
|
||||||
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
|
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
|
||||||
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
|
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/move.json", bindApi(api.TransactionTags.TagMoveHandler))
|
||||||
apiV1Route.POST("/transaction/tags/delete.json", bindApi(api.TransactionTags.TagDeleteHandler))
|
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
|
// Exchange Rates
|
||||||
apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler))
|
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)
|
listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort)
|
||||||
|
|
||||||
if config.Protocol == settings.SCHEME_SOCKET {
|
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)
|
err = router.RunUnix(config.UnixSocketPath)
|
||||||
} else if config.Protocol == settings.SCHEME_HTTP {
|
} 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)
|
err = router.Run(listenAddr)
|
||||||
} else if config.Protocol == settings.SCHEME_HTTPS {
|
} 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)
|
err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile)
|
||||||
} else {
|
} else {
|
||||||
err = errs.ErrInvalidProtocol
|
err = errs.ErrInvalidProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[server.startWebServer] cannot start, because %s", err)
|
log.BootErrorf("[webserver.startWebServer] cannot start, because %s", err)
|
||||||
return 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 {
|
func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
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
@@ -25,16 +25,16 @@ root_url = %(protocol)s://%(domain)s:%(http_port)s/
|
|||||||
cert_file =
|
cert_file =
|
||||||
cert_key_file =
|
cert_key_file =
|
||||||
|
|
||||||
# Unix socket path, for "socket" only
|
# Unix socket path, for "socket" protocol only
|
||||||
unix_socket =
|
unix_socket =
|
||||||
|
|
||||||
# Static file root path (relative or absolute)
|
# Static file root path (relative or absolute path)
|
||||||
static_root_path = public
|
static_root_path = public
|
||||||
|
|
||||||
# Enable GZip
|
# Enable GZip
|
||||||
enable_gzip = false
|
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
|
log_request = true
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
@@ -47,27 +47,40 @@ name = ezbookkeeping
|
|||||||
user = root
|
user = root
|
||||||
passwd =
|
passwd =
|
||||||
|
|
||||||
# For "postgres" only, Either "disable", "require" or "verify-full"
|
# For "postgres" database only, Either "disable", "require" or "verify-full"
|
||||||
ssl_mode = disable
|
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
|
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_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_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
|
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
|
log_query = false
|
||||||
|
|
||||||
# Set to true to automatically update database structure when starting web server
|
# Set to true to automatically update database structure when starting web server
|
||||||
auto_update_database = true
|
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]
|
[log]
|
||||||
# Either "console", "file", default is "console"
|
# Either "console", "file", default is "console"
|
||||||
# Use space to separate multiple modes, e.g. "console file"
|
# 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"
|
# Either "debug", "info", "warn", "error", default is "info"
|
||||||
level = 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
|
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]
|
||||||
# Uuid generator type, supports "internal" currently
|
# Uuid generator type, supports "internal" currently
|
||||||
generator_type = internal
|
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
|
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]
|
[security]
|
||||||
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
||||||
secret_key =
|
secret_key =
|
||||||
|
|
||||||
# Set to true to enable two factor authorization
|
# Set to true to enable two-factor authorization
|
||||||
enable_two_factor = true
|
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
|
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
|
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
|
# Add X-Request-Id header to response to track user request or error, default is true
|
||||||
request_id_header = true
|
request_id_header = true
|
||||||
|
|
||||||
@@ -106,13 +180,131 @@ request_id_header = true
|
|||||||
# Set to true to allow users to register account by themselves
|
# Set to true to allow users to register account by themselves
|
||||||
enable_register = true
|
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]
|
[data]
|
||||||
# Set to true to allow users to export their data
|
# Set to true to allow users to export their data
|
||||||
enable_export = true
|
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]
|
||||||
# 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
|
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
|
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
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/cmd"
|
"github.com/mayswind/ezbookkeeping/cmd"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
settings.Version = Version
|
||||||
|
settings.CommitHash = CommitHash
|
||||||
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "ezBookkeeping",
|
Name: "ezBookkeeping",
|
||||||
Usage: "A lightweight personal bookkeeping app hosted by yourself.",
|
Usage: "A lightweight personal bookkeeping app hosted by yourself.",
|
||||||
@@ -32,12 +36,18 @@ func main() {
|
|||||||
cmd.WebServer,
|
cmd.WebServer,
|
||||||
cmd.Database,
|
cmd.Database,
|
||||||
cmd.UserData,
|
cmd.UserData,
|
||||||
|
cmd.SecurityUtils,
|
||||||
|
cmd.Utilities,
|
||||||
},
|
},
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "conf-path",
|
Name: "conf-path",
|
||||||
Usage: "Custom config `FILE` path",
|
Usage: "Custom config `FILE` path",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "no-boot-log",
|
||||||
|
Usage: "Disable boot log",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,78 @@
|
|||||||
module github.com/mayswind/ezbookkeeping
|
module github.com/mayswind/ezbookkeeping
|
||||||
|
|
||||||
go 1.14
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/boombuler/barcode v1.0.2
|
||||||
github.com/gin-contrib/gzip v0.0.3
|
github.com/gin-contrib/cache v1.3.0
|
||||||
github.com/gin-gonic/gin v1.6.3
|
github.com/gin-contrib/gzip v1.0.1
|
||||||
github.com/go-playground/validator/v10 v10.4.1
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-sql-driver/mysql v1.5.0
|
github.com/go-playground/validator/v10 v10.22.0
|
||||||
github.com/lib/pq v1.8.0
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.4
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/pquerna/otp v1.3.0
|
github.com/lib/pq v1.10.9
|
||||||
github.com/sirupsen/logrus v1.7.0
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
github.com/minio/minio-go/v7 v7.0.74
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/urfave/cli/v2 v2.3.0
|
github.com/pquerna/otp v1.4.0
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
github.com/sirupsen/logrus v1.9.3
|
||||||
gopkg.in/ini.v1 v1.62.0
|
github.com/stretchr/testify v1.9.0
|
||||||
xorm.io/xorm v1.0.5
|
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||||
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/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||||
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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
|
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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
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.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
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.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.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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
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/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
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.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/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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||||
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.6.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 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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-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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
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/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.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=
|
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=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
xorm.io/xorm v1.0.5 h1:LRr5PfOUb4ODPR63YwbowkNDwcolT2LnkwP/TUaMaB0=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
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=
|
||||||
|
|||||||
Generated
+7992
-12562
File diff suppressed because it is too large
Load Diff
+40
-44
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ezbookkeeping",
|
"name": "ezbookkeeping",
|
||||||
"version": "0.1.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -12,56 +12,52 @@
|
|||||||
"url": "https://github.com/mayswind/ezbookkeeping/issues"
|
"url": "https://github.com/mayswind/ezbookkeeping/issues"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "cross-env NODE_ENV=development vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "cross-env NODE_ENV=production vite build",
|
||||||
"lint": "vue-cli-service lint"
|
"serve:dist": "vite preview",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"cbor-js": "^0.1.0",
|
||||||
"core-js": "^3.6.5",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.2.0",
|
||||||
"framework7": "^5.7.14",
|
"dom7": "^4.0.6",
|
||||||
"framework7-icons": "^3.0.1",
|
"echarts": "^5.5.1",
|
||||||
"framework7-vue": "^5.7.14",
|
"framework7": "^8.3.3",
|
||||||
"js-cookie": "^2.2.1",
|
"framework7-icons": "^5.0.5",
|
||||||
|
"framework7-vue": "^8.3.3",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"line-awesome": "^1.3.0",
|
"line-awesome": "^1.3.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.33",
|
"moment-timezone": "^0.5.45",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"ua-parser-js": "^0.7.28",
|
"skeleton-elements": "^4.0.1",
|
||||||
"vue": "^2.6.12",
|
"swiper": "^10.2.0",
|
||||||
"vue-clipboard2": "^0.3.1",
|
"ua-parser-js": "^1.0.38",
|
||||||
"vue-i18n": "^8.24.3",
|
"vue": "^3.4.31",
|
||||||
"vue-pincode-input": "^0.4.0",
|
"vue-echarts": "^6.7.3",
|
||||||
"vuex": "^3.6.2"
|
"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": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.5.11",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/cli-plugin-eslint": "^4.5.11",
|
"cross-env": "^7.0.3",
|
||||||
"@vue/cli-plugin-pwa": "^4.5.11",
|
"eslint": "^8.57.0",
|
||||||
"@vue/cli-service": "^4.5.11",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"git-rev-sync": "^3.0.2",
|
||||||
"babel-plugin-component": "^1.1.1",
|
"postcss-preset-env": "^9.5.16",
|
||||||
"eslint": "^6.7.2",
|
"sass": "^1.77.6",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"vite": "^5.3.3",
|
||||||
"git-revision-webpack-plugin": "^3.0.6",
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
"moment-locales-webpack-plugin": "^1.2.0",
|
"vite-plugin-vuetify": "^2.0.3"
|
||||||
"vue-template-compiler": "^2.6.12"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"plugin:vue/essential",
|
|
||||||
"eslint:recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"parser": "babel-eslint"
|
|
||||||
},
|
|
||||||
"rules": {}
|
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
+84
-27
@@ -4,10 +4,13 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +27,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AccountListHandler returns accounts list of current user
|
// 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
|
var accountListReq models.AccountListRequest
|
||||||
err := c.ShouldBindQuery(&accountListReq)
|
err := c.ShouldBindQuery(&accountListReq)
|
||||||
|
|
||||||
@@ -34,11 +37,11 @@ func (a *AccountsApi) AccountListHandler(c *core.Context) (interface{}, *errs.Er
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
accounts, err := a.accounts.GetAllAccountsByUid(uid)
|
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountListHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
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))
|
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
|
// 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
|
var accountGetReq models.AccountGetRequest
|
||||||
err := c.ShouldBindQuery(&accountGetReq)
|
err := c.ShouldBindQuery(&accountGetReq)
|
||||||
|
|
||||||
@@ -94,11 +97,11 @@ func (a *AccountsApi) AccountGetHandler(c *core.Context) (interface{}, *errs.Err
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(uid, accountGetReq.Id)
|
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountGetReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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)
|
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
|
// 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
|
var accountCreateReq models.AccountCreateRequest
|
||||||
err := c.ShouldBindJSON(&accountCreateReq)
|
err := c.ShouldBindJSON(&accountCreateReq)
|
||||||
|
|
||||||
@@ -136,9 +139,21 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
|
|||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
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 accountCreateReq.Type == models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
|
||||||
if len(accountCreateReq.SubAccounts) > 0 {
|
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
|
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 {
|
} else if accountCreateReq.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
|
||||||
if len(accountCreateReq.SubAccounts) < 1 {
|
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
|
return nil, errs.ErrAccountHaveNoSubAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,17 +181,17 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
|
|||||||
subAccount := accountCreateReq.SubAccounts[i]
|
subAccount := accountCreateReq.SubAccounts[i]
|
||||||
|
|
||||||
if subAccount.Category != accountCreateReq.Category {
|
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
|
return nil, errs.ErrSubAccountCategoryNotEqualsToParent
|
||||||
}
|
}
|
||||||
|
|
||||||
if subAccount.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
|
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
|
return nil, errs.ErrSubAccountTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if subAccount.Currency == validators.ParentAccountCurrencyPlaceholder {
|
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
|
return nil, errs.ErrAccountCurrencyInvalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,17 +201,53 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
maxOrderId, err := a.accounts.GetMaxDisplayOrder(uid, accountCreateReq.Category)
|
maxOrderId, err := a.accounts.GetMaxDisplayOrder(c, uid, accountCreateReq.Category)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
|
||||||
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
|
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 {
|
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())
|
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)
|
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()
|
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
||||||
|
|
||||||
if len(childrenAccounts) > 0 {
|
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
|
// 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
|
var accountModifyReq models.AccountModifyRequest
|
||||||
err := c.ShouldBindJSON(&accountModifyReq)
|
err := c.ShouldBindJSON(&accountModifyReq)
|
||||||
|
|
||||||
@@ -228,12 +280,17 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.
|
|||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
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()
|
uid := c.GetCurrentUid()
|
||||||
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(uid, accountModifyReq.Id)
|
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountModifyReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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)
|
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
|
||||||
@@ -275,7 +332,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (interface{}, *errs.
|
|||||||
return nil, errs.ErrNothingWillBeUpdated
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.accounts.ModifyAccounts(uid, toUpdateAccounts)
|
err = a.accounts.ModifyAccounts(c, uid, toUpdateAccounts)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
// 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
|
var accountHideReq models.AccountHideRequest
|
||||||
err := c.ShouldBindJSON(&accountHideReq)
|
err := c.ShouldBindJSON(&accountHideReq)
|
||||||
|
|
||||||
@@ -335,7 +392,7 @@ func (a *AccountsApi) AccountHideHandler(c *core.Context) (interface{}, *errs.Er
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
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 {
|
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())
|
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
|
// 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
|
var accountMoveReq models.AccountMoveRequest
|
||||||
err := c.ShouldBindJSON(&accountMoveReq)
|
err := c.ShouldBindJSON(&accountMoveReq)
|
||||||
|
|
||||||
@@ -370,7 +427,7 @@ func (a *AccountsApi) AccountMoveHandler(c *core.Context) (interface{}, *errs.Er
|
|||||||
accounts[i] = account
|
accounts[i] = account
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.accounts.ModifyAccountDisplayOrders(uid, accounts)
|
err = a.accounts.ModifyAccountDisplayOrders(c, uid, accounts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountMoveHandler] failed to move accounts for user \"uid:%d\", because %s", uid, err.Error())
|
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
|
// 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
|
var accountDeleteReq models.AccountDeleteRequest
|
||||||
err := c.ShouldBindJSON(&accountDeleteReq)
|
err := c.ShouldBindJSON(&accountDeleteReq)
|
||||||
|
|
||||||
@@ -392,7 +449,7 @@ func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (interface{}, *errs.
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
err = a.accounts.DeleteAccount(uid, accountDeleteReq.Id)
|
err = a.accounts.DeleteAccount(c, uid, accountDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
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{
|
return &models.Account{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
Name: accountCreateReq.Name,
|
Name: accountCreateReq.Name,
|
||||||
@@ -425,7 +482,7 @@ func (a *AccountsApi) createSubAccountModels(uid int64, accountCreateReq *models
|
|||||||
|
|
||||||
childrenAccounts := make([]*models.Account, len(accountCreateReq.SubAccounts))
|
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)
|
childrenAccounts[i] = a.createNewAccountModel(uid, accountCreateReq.SubAccounts[i], i+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthorizationsApi represents authorization api
|
// AuthorizationsApi represents authorization api
|
||||||
@@ -27,7 +28,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AuthorizeHandler verifies and authorizes current login request
|
// 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
|
var credential models.UserLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
@@ -36,14 +37,35 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
|
|||||||
return nil, errs.ErrLoginNameOrPasswordInvalid
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
||||||
return nil, errs.ErrLoginNameOrPasswordWrong
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
|
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
|
twoFactorEnable := a.tokens.CurrentConfig().EnableTwoFactor
|
||||||
|
|
||||||
if twoFactorEnable {
|
if twoFactorEnable {
|
||||||
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(user.Uid)
|
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, user.Uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,9 +86,9 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
|
|||||||
var claims *core.UserTokenClaims
|
var claims *core.UserTokenClaims
|
||||||
|
|
||||||
if twoFactorEnable {
|
if twoFactorEnable {
|
||||||
token, claims, err = a.tokens.CreateRequire2FAToken(user, c)
|
token, claims, err = a.tokens.CreateRequire2FAToken(c, user)
|
||||||
} else {
|
} else {
|
||||||
token, claims, err = a.tokens.CreateToken(user, c)
|
token, claims, err = a.tokens.CreateToken(c, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,16 +96,20 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
|
|||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !twoFactorEnable {
|
||||||
|
c.SetTextualToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
c.SetTokenClaims(claims)
|
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)
|
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
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
|
// 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
|
var credential models.TwoFactorLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
@@ -93,10 +119,10 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,37 +131,48 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac
|
|||||||
return nil, errs.ErrPasscodeInvalid
|
return nil, errs.ErrPasscodeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrUserNotFound
|
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()
|
oldTokenClaims := c.GetTokenClaims()
|
||||||
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
|
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
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
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
|
// 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
|
var credential models.TwoFactorRecoveryCodeLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
@@ -145,10 +182,10 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,46 +193,58 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
|
|||||||
return nil, errs.ErrTwoFactorIsNotEnabled
|
return nil, errs.ErrTwoFactorIsNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrUserNotFound
|
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 {
|
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)
|
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
oldTokenClaims := c.GetTokenClaims()
|
oldTokenClaims := c.GetTokenClaims()
|
||||||
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
|
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
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
|
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{
|
return &models.AuthResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
Need2FA: need2FA,
|
Need2FA: need2FA,
|
||||||
User: user.ToUserBasicInfo(),
|
User: user.ToUserBasicInfo(),
|
||||||
|
NotificationContent: settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-76
@@ -19,30 +19,149 @@ const pageCountForDataExport = 1000
|
|||||||
|
|
||||||
// DataManagementsApi represents data management api
|
// DataManagementsApi represents data management api
|
||||||
type DataManagementsApi struct {
|
type DataManagementsApi struct {
|
||||||
exporter *converters.EzBookKeepingCSVFileExporter
|
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
|
||||||
tokens *services.TokenService
|
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
|
||||||
users *services.UserService
|
tokens *services.TokenService
|
||||||
accounts *services.AccountService
|
users *services.UserService
|
||||||
transactions *services.TransactionService
|
accounts *services.AccountService
|
||||||
categories *services.TransactionCategoryService
|
transactions *services.TransactionService
|
||||||
tags *services.TransactionTagService
|
categories *services.TransactionCategoryService
|
||||||
|
tags *services.TransactionTagService
|
||||||
|
templates *services.TransactionTemplateService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a data management api singleton instance
|
// Initialize a data management api singleton instance
|
||||||
var (
|
var (
|
||||||
DataManagements = &DataManagementsApi{
|
DataManagements = &DataManagementsApi{
|
||||||
exporter: &converters.EzBookKeepingCSVFileExporter{},
|
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
|
||||||
tokens: services.Tokens,
|
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
|
||||||
users: services.Users,
|
tokens: services.Tokens,
|
||||||
accounts: services.Accounts,
|
users: services.Users,
|
||||||
transactions: services.Transactions,
|
accounts: services.Accounts,
|
||||||
categories: services.TransactionCategories,
|
transactions: services.Transactions,
|
||||||
tags: services.TransactionTags,
|
categories: services.TransactionCategories,
|
||||||
|
tags: services.TransactionTags,
|
||||||
|
templates: services.TransactionTemplates,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExportDataHandler returns exported data in csv format
|
// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format
|
||||||
func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string, *errs.Error) {
|
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 {
|
if !settings.Container.Current.EnableDataExport {
|
||||||
return nil, "", errs.ErrDataExportNotAllowed
|
return nil, "", errs.ErrDataExportNotAllowed
|
||||||
}
|
}
|
||||||
@@ -57,7 +176,7 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -67,28 +186,28 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
|
|||||||
return nil, "", errs.ErrUserNotFound
|
return nil, "", errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts, err := a.accounts.GetAllAccountsByUid(uid)
|
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
return nil, "", errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
categories, err := a.categories.GetAllCategoriesByUid(uid, 0, -1)
|
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
return nil, "", errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
tags, err := a.tags.GetAllTagsByUid(uid)
|
tags, err := a.tags.GetAllTagsByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
return nil, "", errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(uid)
|
tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
categoryMap := a.categories.GetCategoryMapByList(categories)
|
||||||
tagMap := a.tags.GetTagMapByList(tags)
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
|
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
|
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)
|
return nil, "", errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := a.getFileName(user, timezone)
|
fileName := a.getFileName(user, timezone, fileType)
|
||||||
|
|
||||||
return result, fileName, nil
|
return result, fileName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearDataHandler deletes all user data
|
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string {
|
||||||
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 {
|
|
||||||
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
|
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
|
||||||
currentTime = strings.Replace(currentTime, "-", "_", -1)
|
currentTime = strings.Replace(currentTime, "-", "_", -1)
|
||||||
currentTime = strings.Replace(currentTime, " ", "_", -1)
|
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
@@ -14,11 +14,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ApiNotFound returns api not found error
|
// 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
|
return nil, errs.ErrApiNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// MethodNotAllowed returns method not allowed error
|
// 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
|
return nil, errs.ErrMethodNotAllowed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
@@ -12,6 +14,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExchangeRatesApi represents exchange rate api
|
// ExchangeRatesApi represents exchange rate api
|
||||||
@@ -23,7 +26,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// LatestExchangeRateHandler returns latest exchange rate data
|
// 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
|
dataSource := exchangerates.Container.Current
|
||||||
|
|
||||||
if dataSource == nil {
|
if dataSource == nil {
|
||||||
@@ -32,15 +35,28 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (interface
|
|||||||
|
|
||||||
uid := c.GetCurrentUid()
|
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{
|
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()
|
urls := dataSource.GetRequestUrls()
|
||||||
exchangeRateResps := make([]*models.LatestExchangeRateResponse, 0, len(urls))
|
exchangeRateResps := make([]*models.LatestExchangeRateResponse, 0, len(urls))
|
||||||
|
|
||||||
for i := 0; i < len(urls); i++ {
|
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 {
|
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())
|
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()
|
defer resp.Body.Close()
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
exchangeRateResp, err := dataSource.Parse(c, body)
|
exchangeRateResp, err := dataSource.Parse(c, body)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -2,12 +2,14 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,13 +28,13 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TokenListHandler returns available token list of current user
|
// 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()
|
uid := c.GetCurrentUid()
|
||||||
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(uid)
|
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
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))
|
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),
|
TokenId: a.tokens.GenerateTokenId(token),
|
||||||
TokenType: token.TokenType,
|
TokenType: token.TokenType,
|
||||||
UserAgent: token.UserAgent,
|
UserAgent: token.UserAgent,
|
||||||
CreatedAt: token.CreatedUnixTime,
|
LastSeen: token.LastSeenUnixTime,
|
||||||
ExpiredAt: token.ExpiredUnixTime,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
|
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
|
||||||
tokenResp.IsCurrent = true
|
tokenResp.IsCurrent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,18 +62,11 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TokenRevokeCurrentHandler revokes current token of current user
|
// TokenRevokeCurrentHandler revokes current token of current user
|
||||||
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *errs.Error) {
|
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (any, *errs.Error) {
|
||||||
_, claims, err := a.tokens.ParseToken(c)
|
_, claims, err := a.tokens.ParseTokenByHeader(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.Or(err, 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
|
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
|
||||||
@@ -83,25 +77,25 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *er
|
|||||||
}
|
}
|
||||||
|
|
||||||
tokenRecord := &models.TokenRecord{
|
tokenRecord := &models.TokenRecord{
|
||||||
Uid: uid,
|
Uid: claims.Uid,
|
||||||
UserTokenId: userTokenId,
|
UserTokenId: userTokenId,
|
||||||
CreatedUnixTime: claims.IssuedAt,
|
CreatedUnixTime: claims.IssuedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenId := a.tokens.GenerateTokenId(tokenRecord)
|
tokenId := a.tokens.GenerateTokenId(tokenRecord)
|
||||||
err = a.tokens.DeleteToken(tokenRecord)
|
err = a.tokens.DeleteToken(c, tokenRecord)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenRevokeHandler revokes specific token of current user
|
// 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
|
var tokenRevokeReq models.TokenRevokeRequest
|
||||||
err := c.ShouldBindJSON(&tokenRevokeReq)
|
err := c.ShouldBindJSON(&tokenRevokeReq)
|
||||||
|
|
||||||
@@ -127,7 +121,7 @@ func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Erro
|
|||||||
return nil, errs.ErrInvalidTokenId
|
return nil, errs.ErrInvalidTokenId
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.tokens.DeleteToken(tokenRecord)
|
err = a.tokens.DeleteToken(c, tokenRecord)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
// 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()
|
uid := c.GetCurrentUid()
|
||||||
tokens, err := a.tokens.GetAllTokensByUid(uid)
|
tokens, err := a.tokens.GetAllTokensByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
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()
|
claims := c.GetTokenClaims()
|
||||||
@@ -154,7 +148,7 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
|
|||||||
for i := 0; i < len(tokens); i++ {
|
for i := 0; i < len(tokens); i++ {
|
||||||
token := 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
|
currentTokenIndex = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -162,7 +156,7 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
|
|||||||
|
|
||||||
tokens = append(tokens[:currentTokenIndex], tokens[currentTokenIndex+1:]...)
|
tokens = append(tokens[:currentTokenIndex], tokens[currentTokenIndex+1:]...)
|
||||||
|
|
||||||
err = a.tokens.DeleteTokens(uid, tokens)
|
err = a.tokens.DeleteTokens(c, uid, tokens)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
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
|
// 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()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||||
return nil, errs.ErrUserNotFound
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrTokenGenerating)
|
||||||
}
|
}
|
||||||
|
|
||||||
oldTokenClaims := c.GetTokenClaims()
|
|
||||||
oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
||||||
oldTokenRecord := &models.TokenRecord{
|
oldTokenRecord := &models.TokenRecord{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
@@ -198,14 +225,16 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (interface{}, *errs.Err
|
|||||||
CreatedUnixTime: oldTokenClaims.IssuedAt,
|
CreatedUnixTime: oldTokenClaims.IssuedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
refreshResp := &models.TokenRefreshResponse{
|
refreshResp := &models.TokenRefreshResponse{
|
||||||
NewToken: token,
|
NewToken: token,
|
||||||
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
|
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
|
||||||
User: user.ToUserBasicInfo(),
|
User: user.ToUserBasicInfo(),
|
||||||
|
NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshResp, nil
|
return refreshResp, nil
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ package api
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TransactionCategoriesApi represents transaction category api
|
// TransactionCategoriesApi represents transaction category api
|
||||||
@@ -23,7 +28,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CategoryListHandler returns transaction category list of current user
|
// 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
|
var categoryListReq models.TransactionCategoryListRequest
|
||||||
err := c.ShouldBindQuery(&categoryListReq)
|
err := c.ShouldBindQuery(&categoryListReq)
|
||||||
|
|
||||||
@@ -33,18 +38,18 @@ func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (interfa
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryListHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
return a.getTransactionCategoryListByTypeResponse(categories, categoryListReq.ParentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoryGetHandler returns one specific transaction category of current user
|
// 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
|
var categoryGetReq models.TransactionCategoryGetRequest
|
||||||
err := c.ShouldBindQuery(&categoryGetReq)
|
err := c.ShouldBindQuery(&categoryGetReq)
|
||||||
|
|
||||||
@@ -54,11 +59,11 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
category, err := a.categories.GetCategoryByCategoryId(uid, categoryGetReq.Id)
|
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryGetReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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()
|
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
|
// 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
|
var categoryCreateReq models.TransactionCategoryCreateRequest
|
||||||
err := c.ShouldBindJSON(&categoryCreateReq)
|
err := c.ShouldBindJSON(&categoryCreateReq)
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (inter
|
|||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
if categoryCreateReq.ParentId > 0 {
|
if categoryCreateReq.ParentId > 0 {
|
||||||
parentCategory, err := a.categories.GetCategoryByCategoryId(uid, categoryCreateReq.ParentId)
|
parentCategory, err := a.categories.GetCategoryByCategoryId(c, uid, categoryCreateReq.ParentId)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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 {
|
if categoryCreateReq.ParentId <= 0 {
|
||||||
maxOrderId, err = a.categories.GetMaxDisplayOrder(uid, categoryCreateReq.Type)
|
maxOrderId, err = a.categories.GetMaxDisplayOrder(c, uid, categoryCreateReq.Type)
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
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 {
|
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())
|
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)
|
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()
|
categoryResp := category.ToTransactionCategoryInfoResponse()
|
||||||
|
|
||||||
return categoryResp, nil
|
return categoryResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoryCreateBatchHandler saves some new transaction category by request parameters for current user
|
// 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
|
var categoryCreateBatchReq models.TransactionCategoryCreateBatchRequest
|
||||||
err := c.ShouldBindJSON(&categoryCreateBatchReq)
|
err := c.ShouldBindBodyWith(&categoryCreateBatchReq, binding.JSON)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] parse request failed, because %s", err.Error())
|
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()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
categoryTypeMaxOrderMap := make(map[models.TransactionCategoryType]int)
|
categories, err := a.createBatchCategories(c, uid, &categoryCreateBatchReq)
|
||||||
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)
|
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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)
|
return a.getTransactionCategoryListByTypeResponse(categories, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoryModifyHandler saves an existed transaction category by request parameters for current user
|
// 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
|
var categoryModifyReq models.TransactionCategoryModifyRequest
|
||||||
err := c.ShouldBindJSON(&categoryModifyReq)
|
err := c.ShouldBindJSON(&categoryModifyReq)
|
||||||
|
|
||||||
@@ -204,24 +191,26 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
category, err := a.categories.GetCategoryByCategoryId(uid, categoryModifyReq.Id)
|
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryModifyReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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{
|
newCategory := &models.TransactionCategory{
|
||||||
CategoryId: category.CategoryId,
|
CategoryId: category.CategoryId,
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
Name: categoryModifyReq.Name,
|
ParentCategoryId: categoryModifyReq.ParentId,
|
||||||
Icon: categoryModifyReq.Icon,
|
Name: categoryModifyReq.Name,
|
||||||
Color: categoryModifyReq.Color,
|
Icon: categoryModifyReq.Icon,
|
||||||
Comment: categoryModifyReq.Comment,
|
Color: categoryModifyReq.Color,
|
||||||
Hidden: categoryModifyReq.Hidden,
|
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.Icon == category.Icon &&
|
||||||
newCategory.Color == category.Color &&
|
newCategory.Color == category.Color &&
|
||||||
newCategory.Comment == category.Comment &&
|
newCategory.Comment == category.Comment &&
|
||||||
@@ -229,7 +218,39 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (inter
|
|||||||
return nil, errs.ErrNothingWillBeUpdated
|
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 {
|
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())
|
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)
|
log.InfofWithRequestId(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
|
||||||
|
|
||||||
newCategory.Type = category.Type
|
newCategory.Type = category.Type
|
||||||
newCategory.ParentCategoryId = category.ParentCategoryId
|
|
||||||
newCategory.DisplayOrder = category.DisplayOrder
|
newCategory.DisplayOrder = category.DisplayOrder
|
||||||
categoryResp := newCategory.ToTransactionCategoryInfoResponse()
|
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
|
// 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
|
var categoryHideReq models.TransactionCategoryHideRequest
|
||||||
err := c.ShouldBindJSON(&categoryHideReq)
|
err := c.ShouldBindJSON(&categoryHideReq)
|
||||||
|
|
||||||
@@ -257,7 +277,7 @@ func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interfa
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
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 {
|
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())
|
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
|
// 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
|
var categoryMoveReq models.TransactionCategoryMoveRequest
|
||||||
err := c.ShouldBindJSON(&categoryMoveReq)
|
err := c.ShouldBindJSON(&categoryMoveReq)
|
||||||
|
|
||||||
@@ -292,7 +312,7 @@ func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interfa
|
|||||||
categories[i] = category
|
categories[i] = category
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.categories.ModifyCategoryDisplayOrders(uid, categories)
|
err = a.categories.ModifyCategoryDisplayOrders(c, uid, categories)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error())
|
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
|
// 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
|
var categoryDeleteReq models.TransactionCategoryDeleteRequest
|
||||||
err := c.ShouldBindJSON(&categoryDeleteReq)
|
err := c.ShouldBindJSON(&categoryDeleteReq)
|
||||||
|
|
||||||
@@ -314,7 +334,7 @@ func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
err = a.categories.DeleteCategory(uid, categoryDeleteReq.Id)
|
err = a.categories.DeleteCategory(c, uid, categoryDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
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{
|
return &models.TransactionCategory{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
Name: categoryCreateReq.Name,
|
Name: categoryCreateReq.Name,
|
||||||
|
|||||||
+27
-27
@@ -23,13 +23,13 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TagListHandler returns transaction tag list of current user
|
// 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()
|
uid := c.GetCurrentUid()
|
||||||
tags, err := a.tags.GetAllTagsByUid(uid)
|
tags, err := a.tags.GetAllTagsByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagListHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
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))
|
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
|
// 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
|
var tagGetReq models.TransactionTagGetRequest
|
||||||
err := c.ShouldBindQuery(&tagGetReq)
|
err := c.ShouldBindQuery(&tagGetReq)
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (interface{}, *errs.
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
tag, err := a.tags.GetTagByTagId(uid, tagGetReq.Id)
|
tag, err := a.tags.GetTagByTagId(c, uid, tagGetReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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()
|
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
|
// 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
|
var tagCreateReq models.TransactionTagCreateRequest
|
||||||
err := c.ShouldBindJSON(&tagCreateReq)
|
err := c.ShouldBindJSON(&tagCreateReq)
|
||||||
|
|
||||||
@@ -78,16 +78,16 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (interface{}, *er
|
|||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(uid)
|
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
tag := a.createNewTagModel(uid, &tagCreateReq, maxOrderId+1)
|
||||||
|
|
||||||
err = a.tags.CreateTag(tag)
|
err = a.tags.CreateTag(c, tag)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
// 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
|
var tagModifyReq models.TransactionTagModifyRequest
|
||||||
err := c.ShouldBindJSON(&tagModifyReq)
|
err := c.ShouldBindJSON(&tagModifyReq)
|
||||||
|
|
||||||
@@ -112,11 +112,11 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *er
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
tag, err := a.tags.GetTagByTagId(uid, tagModifyReq.Id)
|
tag, err := a.tags.GetTagByTagId(c, uid, tagModifyReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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{
|
newTag := &models.TransactionTag{
|
||||||
@@ -129,7 +129,7 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (interface{}, *er
|
|||||||
return nil, errs.ErrNothingWillBeUpdated
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.tags.ModifyTag(newTag)
|
err = a.tags.ModifyTag(c, newTag)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
// 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
|
var tagHideReq models.TransactionTagHideRequest
|
||||||
err := c.ShouldBindJSON(&tagHideReq)
|
err := c.ShouldBindJSON(&tagHideReq)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
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 {
|
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)
|
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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagMoveHandler moves display order of existed transaction tags by request parameters for current user
|
// 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
|
var tagMoveReq models.TransactionTagMoveRequest
|
||||||
err := c.ShouldBindJSON(&tagMoveReq)
|
err := c.ShouldBindJSON(&tagMoveReq)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,19 +190,19 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (interface{}, *errs
|
|||||||
tags[i] = tag
|
tags[i] = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.tags.ModifyTagDisplayOrders(uid, tags)
|
err = a.tags.ModifyTagDisplayOrders(c, uid, tags)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagDeleteHandler deletes an existed transaction tag by request parameters for current user
|
// 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
|
var tagDeleteReq models.TransactionTagDeleteRequest
|
||||||
err := c.ShouldBindJSON(&tagDeleteReq)
|
err := c.ShouldBindJSON(&tagDeleteReq)
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (interface{}, *er
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
err = a.tags.DeleteTag(uid, tagDeleteReq.Id)
|
err = a.tags.DeleteTag(c, uid, tagDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
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{
|
return &models.TransactionTag{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
Name: tagCreateReq.Name,
|
Name: tagCreateReq.Name,
|
||||||
|
|||||||
@@ -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
@@ -4,16 +4,18 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const pageCountForLoadTransactionAmounts = 1000
|
|
||||||
|
|
||||||
// TransactionsApi represents transaction api
|
// TransactionsApi represents transaction api
|
||||||
type TransactionsApi struct {
|
type TransactionsApi struct {
|
||||||
transactions *services.TransactionService
|
transactions *services.TransactionService
|
||||||
@@ -35,7 +37,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TransactionCountHandler returns transaction total count of current user
|
// 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
|
var transactionCountReq models.TransactionCountRequest
|
||||||
err := c.ShouldBindQuery(&transactionCountReq)
|
err := c.ShouldBindQuery(&transactionCountReq)
|
||||||
|
|
||||||
@@ -46,14 +48,38 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{},
|
|||||||
|
|
||||||
uid := c.GetCurrentUid()
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get transaction category error, because %s", err.Error())
|
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{
|
countResp := &models.TransactionCountResponse{
|
||||||
TotalCount: totalCount,
|
TotalCount: totalCount,
|
||||||
@@ -63,7 +89,7 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{},
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TransactionListHandler returns transaction list of current user
|
// 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
|
var transactionListReq models.TransactionListByMaxTimeRequest
|
||||||
err := c.ShouldBindQuery(&transactionListReq)
|
err := c.ShouldBindQuery(&transactionListReq)
|
||||||
|
|
||||||
@@ -80,7 +106,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -90,24 +116,54 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
|
|||||||
return nil, errs.ErrUserNotFound
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get transaction category error, because %s", err.Error())
|
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 {
|
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())
|
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
|
hasMore := false
|
||||||
var nextTimeSequenceId *int64
|
var nextTimeSequenceId *int64
|
||||||
|
|
||||||
if len(transactions) > transactionListReq.Count {
|
if len(transactions) > int(transactionListReq.Count) {
|
||||||
hasMore = true
|
hasMore = true
|
||||||
nextTimeSequenceId = &transactions[transactionListReq.Count].TransactionTime
|
nextTimeSequenceId = &transactions[transactionListReq.Count].TransactionTime
|
||||||
transactions = transactions[:transactionListReq.Count]
|
transactions = transactions[:transactionListReq.Count]
|
||||||
@@ -128,11 +184,15 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
|
|||||||
transactionResps.NextTimeSequenceId = nextTimeSequenceId
|
transactionResps.NextTimeSequenceId = nextTimeSequenceId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if transactionListReq.WithCount {
|
||||||
|
transactionResps.TotalCount = &totalCount
|
||||||
|
}
|
||||||
|
|
||||||
return transactionResps, nil
|
return transactionResps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionMonthListHandler returns transaction list of current user by month
|
// TransactionMonthListHandler returns all transaction list of current user by month
|
||||||
func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interface{}, *errs.Error) {
|
func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (any, *errs.Error) {
|
||||||
var transactionListReq models.TransactionListInMonthByPageRequest
|
var transactionListReq models.TransactionListInMonthByPageRequest
|
||||||
err := c.ShouldBindQuery(&transactionListReq)
|
err := c.ShouldBindQuery(&transactionListReq)
|
||||||
|
|
||||||
@@ -149,7 +209,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -159,25 +219,37 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
|
|||||||
return nil, errs.ErrUserNotFound
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get transaction category error, because %s", err.Error())
|
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 {
|
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())
|
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
|
return nil, errs.Or(err, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionResult, err := a.getTransactionListResult(c, user, transactions, utcOffset, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
|
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{
|
transactionResps := &models.TransactionInfoPageWrapperResponse2{
|
||||||
Items: transactionResult,
|
Items: transactionResult,
|
||||||
TotalCount: totalCount,
|
TotalCount: int64(transactionResult.Len()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return transactionResps, nil
|
return transactionResps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionStatisticsHandler returns transaction statistics of current user
|
// 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
|
var statisticReq models.TransactionStatisticRequest
|
||||||
err := c.ShouldBindQuery(&statisticReq)
|
err := c.ShouldBindQuery(&statisticReq)
|
||||||
|
|
||||||
@@ -205,8 +277,20 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interfa
|
|||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
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()
|
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{
|
statisticResp := &models.TransactionStatisticResponse{
|
||||||
StartTime: statisticReq.StartTime,
|
StartTime: statisticReq.StartTime,
|
||||||
@@ -227,8 +311,66 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interfa
|
|||||||
return statisticResp, nil
|
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
|
// 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
|
var transactionAmountsReq models.TransactionAmountsRequest
|
||||||
err := c.ShouldBindQuery(&transactionAmountsReq)
|
err := c.ShouldBindQuery(&transactionAmountsReq)
|
||||||
|
|
||||||
@@ -249,31 +391,38 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
|
|||||||
return nil, errs.ErrQueryItemsEmpty
|
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")
|
log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are too many items")
|
||||||
return nil, errs.ErrQueryItemsTooMuch
|
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()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
accounts, err := a.accounts.GetAllAccountsByUid(uid)
|
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||||
accountMap := a.accounts.GetAccountMapByList(accounts)
|
accountMap := a.accounts.GetAccountMapByList(accounts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
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++ {
|
for i := 0; i < len(requestItems); i++ {
|
||||||
requestItem := 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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
amountsMap := make(map[string]*models.TransactionAmountsResponseItemAmountInfo)
|
||||||
@@ -322,130 +471,26 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
|
|||||||
amountsMap[account.Currency] = totalAmounts
|
amountsMap[account.Currency] = totalAmounts
|
||||||
}
|
}
|
||||||
|
|
||||||
allTotalAmounts := make([]*models.TransactionAmountsResponseItemAmountInfo, 0)
|
allTotalAmounts := make(models.TransactionAmountsResponseItemAmountInfoSlice, 0)
|
||||||
|
|
||||||
for _, totalAmounts := range amountsMap {
|
for _, totalAmounts := range amountsMap {
|
||||||
allTotalAmounts = append(allTotalAmounts, totalAmounts)
|
allTotalAmounts = append(allTotalAmounts, totalAmounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
amountsResp[requestItem.Name] = &models.TransactionAmountsResponseItem{
|
sort.Sort(allTotalAmounts)
|
||||||
|
|
||||||
|
amountsResp.Set(requestItem.Name, &models.TransactionAmountsResponseItem{
|
||||||
StartTime: requestItem.StartTime,
|
StartTime: requestItem.StartTime,
|
||||||
EndTime: requestItem.EndTime,
|
EndTime: requestItem.EndTime,
|
||||||
Amounts: allTotalAmounts,
|
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
|
return amountsResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionGetHandler returns one specific transaction of current user
|
// 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
|
var transactionGetReq models.TransactionGetRequest
|
||||||
err := c.ShouldBindQuery(&transactionGetReq)
|
err := c.ShouldBindQuery(&transactionGetReq)
|
||||||
|
|
||||||
@@ -462,7 +507,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -472,15 +517,15 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
|
|||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionGetReq.Id)
|
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionGetReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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 {
|
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)
|
accountIds := make([]int64, 0, 2)
|
||||||
@@ -491,7 +536,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
|
|||||||
accountIds = utils.ToUniqueInt64Slice(accountIds)
|
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 {
|
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)
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
|
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 category *models.TransactionCategory
|
||||||
var tagMap map[int64]*models.TransactionTag
|
var tagMap map[int64]*models.TransactionTag
|
||||||
|
|
||||||
if !transactionGetReq.TrimCategory {
|
if !transactionGetReq.TrimCategory {
|
||||||
category, err = a.transactionCategories.GetCategoryByCategoryId(uid, transaction.CategoryId)
|
category, err = a.transactionCategories.GetCategoryByCategoryId(c, uid, transaction.CategoryId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions category for user \"uid:%d\", because %s", uid, err.Error())
|
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 {
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
|
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
|
// 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
|
var transactionCreateReq models.TransactionCreateRequest
|
||||||
err := c.ShouldBindJSON(&transactionCreateReq)
|
err := c.ShouldBindJSON(&transactionCreateReq)
|
||||||
|
|
||||||
@@ -601,7 +646,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -611,14 +656,36 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
|
|||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction := a.createNewTransactionModel(uid, &transactionCreateReq)
|
transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP())
|
||||||
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
|
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
|
||||||
|
|
||||||
if !transactionEditable {
|
if !transactionEditable {
|
||||||
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
|
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 {
|
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())
|
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)
|
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)
|
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
|
||||||
|
|
||||||
return transactionResp, nil
|
return transactionResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionModifyHandler saves an existed transaction by request parameters for current user
|
// 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
|
var transactionModifyReq models.TransactionModifyRequest
|
||||||
err := c.ShouldBindJSON(&transactionModifyReq)
|
err := c.ShouldBindJSON(&transactionModifyReq)
|
||||||
|
|
||||||
@@ -650,7 +718,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -660,7 +728,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
|
|||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionModifyReq.Id)
|
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionModifyReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
|
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]
|
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
|
||||||
@@ -702,6 +770,11 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
|
|||||||
newTransaction.RelatedAccountAmount = transactionModifyReq.DestinationAmount
|
newTransaction.RelatedAccountAmount = transactionModifyReq.DestinationAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if transactionModifyReq.GeoLocation != nil {
|
||||||
|
newTransaction.GeoLongitude = transactionModifyReq.GeoLocation.Longitude
|
||||||
|
newTransaction.GeoLatitude = transactionModifyReq.GeoLocation.Latitude
|
||||||
|
}
|
||||||
|
|
||||||
if newTransaction.CategoryId == transaction.CategoryId &&
|
if newTransaction.CategoryId == transaction.CategoryId &&
|
||||||
utils.GetUnixTimeFromTransactionTime(newTransaction.TransactionTime) == utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) &&
|
utils.GetUnixTimeFromTransactionTime(newTransaction.TransactionTime) == utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) &&
|
||||||
newTransaction.TimezoneUtcOffset == transaction.TimezoneUtcOffset &&
|
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) &&
|
(transaction.Type != models.TRANSACTION_DB_TYPE_TRANSFER_OUT || newTransaction.RelatedAccountAmount == transaction.RelatedAccountAmount) &&
|
||||||
newTransaction.HideAmount == transaction.HideAmount &&
|
newTransaction.HideAmount == transaction.HideAmount &&
|
||||||
newTransaction.Comment == transaction.Comment &&
|
newTransaction.Comment == transaction.Comment &&
|
||||||
|
newTransaction.GeoLongitude == transaction.GeoLongitude &&
|
||||||
|
newTransaction.GeoLatitude == transaction.GeoLatitude &&
|
||||||
utils.Int64SliceEquals(tagIds, transactionTagIds) {
|
utils.Int64SliceEquals(tagIds, transactionTagIds) {
|
||||||
return nil, errs.ErrNothingWillBeUpdated
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
}
|
}
|
||||||
@@ -730,7 +805,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
|
|||||||
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
|
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.transactions.ModifyTransaction(newTransaction, addTransactionTagIds, removeTransactionTagIds)
|
err = a.transactions.ModifyTransaction(c, newTransaction, len(transactionTagIds), addTransactionTagIds, removeTransactionTagIds)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
// 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
|
var transactionDeleteReq models.TransactionDeleteRequest
|
||||||
err := c.ShouldBindJSON(&transactionDeleteReq)
|
err := c.ShouldBindJSON(&transactionDeleteReq)
|
||||||
|
|
||||||
@@ -763,7 +838,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -773,7 +848,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}
|
|||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionDeleteReq.Id)
|
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.transactions.DeleteTransaction(uid, transactionDeleteReq.Id)
|
err = a.transactions.DeleteTransaction(c, uid, transactionDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
return finalTransactions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TransactionsApi) getCategoryAndSubCategoryIds(categoryId int64, uid int64) ([]int64, error) {
|
func (a *TransactionsApi) getAccountOrSubAccountIds(c *core.Context, accountIds string, uid int64) ([]int64, error) {
|
||||||
var allCategoryIds []int64
|
if accountIds == "" || accountIds == "0" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
if categoryId > 0 {
|
requestAccountIds, err := utils.StringArrayToInt64Array(strings.Split(accountIds, ","))
|
||||||
allSubCategories, err := a.transactionCategories.GetAllCategoriesByUid(uid, 0, categoryId)
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allSubCategories) > 0 {
|
accountIdsMap := make(map[int64]int32, len(requestAccountIds))
|
||||||
for i := 0; i < len(allSubCategories); i++ {
|
|
||||||
allCategoryIds = append(allCategoryIds, allSubCategories[i].CategoryId)
|
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
|
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 {
|
func (a *TransactionsApi) getTransactionTagIds(allTransactionTagIds map[int64][]int64) []int64 {
|
||||||
allTagIds := make([]int64, 0, len(allTransactionTagIds))
|
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)
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
|
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
|
var tagMap map[int64]*models.TransactionTag
|
||||||
|
|
||||||
if !trimCategory {
|
if !trimCategory {
|
||||||
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(uid, utils.ToUniqueInt64Slice(categoryIds))
|
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error())
|
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 {
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transactions.getTransactionListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
|
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]
|
transaction := transactions[i]
|
||||||
|
|
||||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
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])
|
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
|
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
|
var transactionDbType models.TransactionDbType
|
||||||
|
|
||||||
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE {
|
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE {
|
||||||
@@ -996,6 +1167,7 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
|
|||||||
Amount: transactionCreateReq.SourceAmount,
|
Amount: transactionCreateReq.SourceAmount,
|
||||||
HideAmount: transactionCreateReq.HideAmount,
|
HideAmount: transactionCreateReq.HideAmount,
|
||||||
Comment: transactionCreateReq.Comment,
|
Comment: transactionCreateReq.Comment,
|
||||||
|
CreatedIp: clientIp,
|
||||||
}
|
}
|
||||||
|
|
||||||
if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER {
|
if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER {
|
||||||
@@ -1003,5 +1175,10 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
|
|||||||
transaction.RelatedAccountAmount = transactionCreateReq.DestinationAmount
|
transaction.RelatedAccountAmount = transactionCreateReq.DestinationAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if transactionCreateReq.GeoLocation != nil {
|
||||||
|
transaction.GeoLongitude = transactionCreateReq.GeoLocation.Longitude
|
||||||
|
transaction.GeoLatitude = transactionCreateReq.GeoLocation.Latitude
|
||||||
|
}
|
||||||
|
|
||||||
return transaction
|
return transaction
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TwoFactorStatusHandler returns 2fa status of current user
|
// 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()
|
uid := c.GetCurrentUid()
|
||||||
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
||||||
|
|
||||||
if err == errs.ErrTwoFactorIsNotEnabled {
|
if err == errs.ErrTwoFactorIsNotEnabled {
|
||||||
statusResp := &models.TwoFactorStatusResponse{
|
statusResp := &models.TwoFactorStatusResponse{
|
||||||
@@ -45,7 +45,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (in
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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
|
// 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()
|
uid := c.GetCurrentUid()
|
||||||
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
|
|||||||
return nil, errs.ErrTwoFactorAlreadyEnabled
|
return nil, errs.ErrTwoFactorAlreadyEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -81,17 +81,17 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
|
|||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(user)
|
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err := key.Image(240, 240)
|
img, err := key.Image(240, 240)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorEnableConfirmHandler enables 2fa for current user
|
// 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
|
var confirmReq models.TwoFactorEnableConfirmRequest
|
||||||
err := c.ShouldBindJSON(&confirmReq)
|
err := c.ShouldBindJSON(&confirmReq)
|
||||||
|
|
||||||
@@ -120,10 +120,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
|
|||||||
return nil, errs.ErrTwoFactorAlreadyEnabled
|
return nil, errs.ErrTwoFactorAlreadyEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -154,28 +154,28 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
|
|||||||
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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 {
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(twoFactorSetting)
|
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(c, twoFactorSetting)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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()
|
now := time.Now().Unix()
|
||||||
err = a.tokens.DeleteTokensBeforeTime(uid, now)
|
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
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())
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
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
|
return confirmResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
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)
|
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
|
// 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
|
var disableReq models.TwoFactorDisableRequest
|
||||||
err := c.ShouldBindJSON(&disableReq)
|
err := c.ShouldBindJSON(&disableReq)
|
||||||
|
|
||||||
@@ -218,7 +219,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -232,10 +233,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
|
|||||||
return nil, errs.ErrUserPasswordWrong
|
return nil, errs.ErrUserPasswordWrong
|
||||||
}
|
}
|
||||||
|
|
||||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,27 +244,27 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
|
|||||||
return nil, errs.ErrTwoFactorIsNotEnabled
|
return nil, errs.ErrTwoFactorIsNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
|
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(uid)
|
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorRecoveryCodeRegenerateHandler returns new 2fa recovery codes and revokes old recovery codes for current user
|
// 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
|
var regenerateReq models.TwoFactorRegenerateRecoveryCodeRequest
|
||||||
err := c.ShouldBindJSON(®enerateReq)
|
err := c.ShouldBindJSON(®enerateReq)
|
||||||
|
|
||||||
@@ -273,7 +274,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -287,10 +288,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
return nil, errs.ErrUserPasswordWrong
|
return nil, errs.ErrUserPasswordWrong
|
||||||
}
|
}
|
||||||
|
|
||||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,14 +302,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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 {
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +317,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
RecoveryCodes: recoveryCodes,
|
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
|
return recoveryCodesResp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+577
-23
@@ -1,40 +1,49 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/locales"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UsersApi represents user api
|
// UsersApi represents user api
|
||||||
type UsersApi struct {
|
type UsersApi struct {
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
|
accounts *services.AccountService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a user api singleton instance
|
// Initialize a user api singleton instance
|
||||||
var (
|
var (
|
||||||
Users = &UsersApi{
|
Users = &UsersApi{
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
|
accounts: services.Accounts,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserRegisterHandler saves a new user by request parameters
|
// 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 {
|
if !settings.Container.Current.EnableUserRegister {
|
||||||
return nil, errs.ErrUserRegistrationNotAllowed
|
return nil, errs.ErrUserRegistrationNotAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
var userRegisterReq models.UserRegisterRequest
|
var userRegisterReq models.UserRegisterRequest
|
||||||
err := c.ShouldBindJSON(&userRegisterReq)
|
err := c.ShouldBindBodyWith(&userRegisterReq, binding.JSON)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
|
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,
|
Email: userRegisterReq.Email,
|
||||||
Nickname: userRegisterReq.Nickname,
|
Nickname: userRegisterReq.Nickname,
|
||||||
Password: userRegisterReq.Password,
|
Password: userRegisterReq.Password,
|
||||||
|
Language: userRegisterReq.Language,
|
||||||
DefaultCurrency: userRegisterReq.DefaultCurrency,
|
DefaultCurrency: userRegisterReq.DefaultCurrency,
|
||||||
FirstDayOfWeek: userRegisterReq.FirstDayOfWeek,
|
FirstDayOfWeek: userRegisterReq.FirstDayOfWeek,
|
||||||
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
|
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.users.CreateUser(user)
|
err = a.users.CreateUser(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
|
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)
|
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
|
||||||
|
|
||||||
authResp := &models.AuthResponse{
|
presetCategoriesSaved := false
|
||||||
Need2FA: false,
|
|
||||||
User: user.ToUserBasicInfo(),
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
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
|
authResp.Token = token
|
||||||
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
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
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserProfileHandler returns user profile of current user
|
// UserEmailVerifyHandler sets user email address verified
|
||||||
func (a *UsersApi) UserProfileHandler(c *core.Context) (interface{}, *errs.Error) {
|
func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
|
||||||
|
var userVerifyEmailReq models.UserVerifyEmailRequest
|
||||||
|
err := c.ShouldBindJSON(&userVerifyEmailReq)
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
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 err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
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
|
// 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
|
var userUpdateReq models.UserProfileUpdateRequest
|
||||||
err := c.ShouldBindJSON(&userUpdateReq)
|
err := c.ShouldBindJSON(&userUpdateReq)
|
||||||
|
|
||||||
@@ -117,7 +227,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
@@ -159,6 +269,45 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
|
|||||||
anythingUpdate = true
|
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 {
|
if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency {
|
||||||
user.DefaultCurrency = userUpdateReq.DefaultCurrency
|
user.DefaultCurrency = userUpdateReq.DefaultCurrency
|
||||||
userNew.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
|
userNew.FirstDayOfWeek = models.WEEKDAY_INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
|
if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat {
|
||||||
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
|
user.LongDateFormat = *userUpdateReq.LongDateFormat
|
||||||
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
|
userNew.LongDateFormat = *userUpdateReq.LongDateFormat
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} 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 {
|
if !anythingUpdate {
|
||||||
return nil, errs.ErrNothingWillBeUpdated
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
keyProfileUpdated, err := a.users.UpdateUser(userNew)
|
keyProfileUpdated, emailSetToUnverified, err := a.users.UpdateUser(c, userNew, modifyUserLanguage)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
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)
|
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
|
||||||
|
|
||||||
resp := &models.UserProfileUpdateResponse{
|
resp := &models.UserProfileUpdateResponse{
|
||||||
User: user.ToUserBasicInfo(),
|
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 {
|
if keyProfileUpdated {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
err = a.tokens.DeleteTokensBeforeTime(uid, now)
|
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
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())
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
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
|
resp.NewToken = token
|
||||||
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
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
|
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
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const pageCountForDataExport = 1000
|
|||||||
// UserDataCli represents user data cli
|
// UserDataCli represents user data cli
|
||||||
type UserDataCli struct {
|
type UserDataCli struct {
|
||||||
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
|
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
|
||||||
|
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
|
||||||
accounts *services.AccountService
|
accounts *services.AccountService
|
||||||
transactions *services.TransactionService
|
transactions *services.TransactionService
|
||||||
categories *services.TransactionCategoryService
|
categories *services.TransactionCategoryService
|
||||||
@@ -26,12 +28,14 @@ type UserDataCli struct {
|
|||||||
users *services.UserService
|
users *services.UserService
|
||||||
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
|
forgetPasswords *services.ForgetPasswordService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize an user data cli singleton instance
|
// Initialize an user data cli singleton instance
|
||||||
var (
|
var (
|
||||||
UserData = &UserDataCli{
|
UserData = &UserDataCli{
|
||||||
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
|
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
|
||||||
|
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
|
||||||
accounts: services.Accounts,
|
accounts: services.Accounts,
|
||||||
transactions: services.Transactions,
|
transactions: services.Transactions,
|
||||||
categories: services.TransactionCategories,
|
categories: services.TransactionCategories,
|
||||||
@@ -39,6 +43,7 @@ var (
|
|||||||
users: services.Users,
|
users: services.Users,
|
||||||
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
||||||
tokens: services.Tokens,
|
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,
|
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := l.users.CreateUser(user)
|
err := l.users.CreateUser(nil, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.AddNewUser] failed to create user \"%s\", because %s", user.Username, err.Error())
|
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
|
return nil, errs.ErrUsernameIsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := l.users.GetUserByUsername(username)
|
user, err := l.users.GetUserByUsername(nil, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.GetUserByUsername] failed to get user by user name \"%s\", because %s", username, err.Error())
|
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
|
return errs.ErrPasswordIsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := l.users.GetUserByUsername(username)
|
user, err := l.users.GetUserByUsername(nil, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.ModifyUserPassword] failed to get user by user name \"%s\", because %s", username, err.Error())
|
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,
|
Password: password,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = l.users.UpdateUser(userNew)
|
_, _, err = l.users.UpdateUser(nil, userNew, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.ModifyUserPassword] failed to update user \"%s\" password, because %s", user.Username, err.Error())
|
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()
|
now := time.Now().Unix()
|
||||||
err = l.tokens.DeleteTokensBeforeTime(user.Uid, now)
|
err = l.tokens.DeleteTokensBeforeTime(nil, user.Uid, now)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.BootInfof("[user_data.ModifyUserPassword] revoke old tokens before unix time \"%d\" for user \"%s\"", now, user.Username)
|
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
|
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
|
// DeleteUser deletes user according to the specified user name
|
||||||
func (l *UserDataCli) DeleteUser(c *cli.Context, username string) error {
|
func (l *UserDataCli) DeleteUser(c *cli.Context, username string) error {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
@@ -168,7 +317,7 @@ func (l *UserDataCli) DeleteUser(c *cli.Context, username string) error {
|
|||||||
return errs.ErrUsernameIsEmpty
|
return errs.ErrUsernameIsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
err := l.users.DeleteUser(username)
|
err := l.users.DeleteUser(nil, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.DeleteUser] failed to delete user by user name \"%s\", because %s", username, err.Error())
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens, err := l.tokens.GetAllUnexpiredNormalTokensByUid(uid)
|
tokens, err := l.tokens.GetAllUnexpiredNormalTokensByUid(nil, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.ListUserTokens] failed to get tokens of user \"%s\", because %s", username, err.Error())
|
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()
|
now := time.Now().Unix()
|
||||||
err = l.tokens.DeleteTokensBeforeTime(uid, now)
|
err = l.tokens.DeleteTokensBeforeTime(nil, uid, now)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.ClearUserTokens] failed to delete tokens of user \"%s\", because %s", username, err.Error())
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
enableTwoFactor, err := l.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
enableTwoFactor, err := l.twoFactorAuthorizations.ExistsTwoFactorSetting(nil, uid)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,17 +401,17 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
|
|||||||
return errs.ErrTwoFactorIsNotEnabled
|
return errs.ErrTwoFactorIsNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
|
err = l.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(nil, uid)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.twoFactorAuthorizations.DeleteTwoFactorSetting(uid)
|
err = l.twoFactorAuthorizations.DeleteTwoFactorSetting(nil, uid)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +432,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
|
|||||||
return false, err
|
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 {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.CheckTransactionAndAccount] failed to get essential data for user \"%s\", because %s", username, err.Error())
|
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 {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.CheckTransactionAndAccount] failed to all transactions for user \"%s\", because %s", username, err.Error())
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.checkTransactionTag(c, transaction.TransactionId, tagIndexs, tagMap)
|
err = l.checkTransactionTag(c, transaction.TransactionId, tagIndexesMap, tagMap)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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 {
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
balance = balance + transaction.Amount
|
balance = balance + transaction.Amount
|
||||||
} else {
|
} 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
|
return false, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,12 +516,12 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !exists && account.Balance != 0 {
|
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
|
return false, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
if account.Balance != actualBalance {
|
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
|
return false, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,7 +530,16 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
|
|||||||
_, exists := accountMap[accountId]
|
_, exists := accountMap[accountId]
|
||||||
|
|
||||||
if !exists {
|
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
|
return false, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,8 +547,74 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
|
|||||||
return true, nil
|
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
|
// 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 == "" {
|
if username == "" {
|
||||||
log.BootErrorf("[user_data.ExportTransaction] user name is empty")
|
log.BootErrorf("[user_data.ExportTransaction] user name is empty")
|
||||||
return nil, errs.ErrUsernameIsEmpty
|
return nil, errs.ErrUsernameIsEmpty
|
||||||
@@ -403,21 +627,29 @@ func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accountMap, categoryMap, tagMap, tagIndexs, err := l.getUserEssentialData(uid, username)
|
accountMap, categoryMap, tagMap, _, tagIndexesMap, err := l.getUserEssentialData(uid, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.ExportTransaction] failed to get essential data for user \"%s\", because %s", username, err.Error())
|
log.BootErrorf("[user_data.ExportTransaction] failed to get essential data for user \"%s\", because %s", username, err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
allTransactions, err := l.transactions.GetAllTransactions(uid, pageCountForDataExport, true)
|
allTransactions, err := l.transactions.GetAllTransactions(nil, uid, pageCountForDataExport, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.ExportTransaction] failed to all transactions for user \"%s\", because %s", username, err.Error())
|
log.BootErrorf("[user_data.ExportTransaction] failed to all transactions for user \"%s\", because %s", username, err.Error())
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.ExportTransaction] failed to get csv format exported data for \"%s\", because %s", username, err.Error())
|
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
|
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 {
|
if uid <= 0 {
|
||||||
log.BootErrorf("[user_data.getUserEssentialData] user uid \"%d\" is invalid", uid)
|
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 {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.getUserEssentialData] failed to get accounts for user \"%s\", because %s", username, err.Error())
|
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)
|
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 {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.getUserEssentialData] failed to get categories for user \"%s\", because %s", username, err.Error())
|
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)
|
categoryMap = l.categories.GetCategoryMapByList(categories)
|
||||||
|
|
||||||
tags, err := l.tags.GetAllTagsByUid(uid)
|
tags, err := l.tags.GetAllTagsByUid(nil, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.getUserEssentialData] failed to get tags for user \"%s\", because %s", username, err.Error())
|
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)
|
tagMap = l.tags.GetTagMapByList(tags)
|
||||||
|
|
||||||
tagIndexs, err = l.tags.GetAllTagIdsOfAllTransactions(uid)
|
tagIndexes, err = l.tags.GetAllTagIdsOfAllTransactions(nil, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.getUserEssentialData] failed to get tag index for user \"%s\", because %s", username, err.Error())
|
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 {
|
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] {
|
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
|
return errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,7 +737,7 @@ func (l *UserDataCli) checkTransactionAccount(c *cli.Context, transaction *model
|
|||||||
}
|
}
|
||||||
|
|
||||||
if relatedAccount.ParentAccountId == models.LevelOneAccountParentId && accountHasChild[relatedAccount.AccountId] {
|
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
|
return errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,15 +770,15 @@ func (l *UserDataCli) checkTransactionCategory(c *cli.Context, transaction *mode
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *UserDataCli) checkTransactionTag(c *cli.Context, transactionId int64, allTagIndexs map[int64][]int64, tagMap map[int64]*models.TransactionTag) error {
|
func (l *UserDataCli) checkTransactionTag(c *cli.Context, transactionId int64, allTagIndexesMap map[int64][]int64, tagMap map[int64]*models.TransactionTag) error {
|
||||||
tagIndexs, exists := allTagIndexs[transactionId]
|
tagIndexes, exists := allTagIndexesMap[transactionId]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(tagIndexs); i++ {
|
for i := 0; i < len(tagIndexes); i++ {
|
||||||
tagIndex := tagIndexs[i]
|
tagIndex := tagIndexes[i]
|
||||||
tag, exists := tagMap[tagIndex]
|
tag, exists := tagMap[tagIndex]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package converters
|
package converters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DataConverter defines the structure of data exporter
|
// DataConverter defines the structure of data exporter
|
||||||
type DataConverter interface {
|
type DataConverter interface {
|
||||||
// ToExportedContent returns the exported data
|
// 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,179 +1,17 @@
|
|||||||
package converters
|
package converters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"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 {
|
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 csvSeparator = ","
|
||||||
const csvDataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"
|
|
||||||
|
|
||||||
// ToExportedContent returns the exported csv data
|
// 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) {
|
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) {
|
||||||
var ret strings.Builder
|
return e.toExportedContent(uid, csvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -9,9 +10,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const requestIdFieldKey = "REQUEST_ID"
|
const requestIdFieldKey = "REQUEST_ID"
|
||||||
|
const textualTokenFieldKey = "TOKEN_STRING"
|
||||||
const tokenClaimsFieldKey = "TOKEN_CLAIMS"
|
const tokenClaimsFieldKey = "TOKEN_CLAIMS"
|
||||||
const responseErrorFieldKey = "RESPONSE_ERROR"
|
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
|
// ClientTimezoneOffsetHeaderName represents the header name of client timezone offset
|
||||||
const ClientTimezoneOffsetHeaderName = "X-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
|
// 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
|
// SetRequestId sets the given request id to context
|
||||||
func (c *Context) SetRequestId(requestId string) {
|
func (c *Context) SetRequestId(requestId string) {
|
||||||
c.Set(requestIdFieldKey, requestId)
|
c.Set(requestIdFieldKey, requestId)
|
||||||
@@ -37,7 +75,23 @@ func (c *Context) GetRequestId() string {
|
|||||||
return requestId.(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) {
|
func (c *Context) SetTokenClaims(claims *UserTokenClaims) {
|
||||||
c.Set(tokenClaimsFieldKey, claims)
|
c.Set(tokenClaimsFieldKey, claims)
|
||||||
}
|
}
|
||||||
@@ -61,13 +115,14 @@ func (c *Context) GetCurrentUid() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
uid, err := strconv.ParseInt(claims.Id, 10, 64)
|
return claims.Uid
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
// GetClientLocale returns the client locale name
|
||||||
return 0
|
func (c *Context) GetClientLocale() string {
|
||||||
}
|
value := c.GetHeader(AcceptLanguageHeaderName)
|
||||||
|
|
||||||
return uid
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientTimezoneOffset returns the client timezone offset
|
// GetClientTimezoneOffset returns the client timezone offset
|
||||||
|
|||||||
+13
-3
@@ -1,12 +1,22 @@
|
|||||||
package core
|
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
|
// MiddlewareHandlerFunc represents the middleware handler function
|
||||||
type MiddlewareHandlerFunc func(*Context)
|
type MiddlewareHandlerFunc func(*Context)
|
||||||
|
|
||||||
// ApiHandlerFunc represents the api handler function
|
// 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)
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecimalSeparator represents the type of decimal separator
|
||||||
|
type DecimalSeparator byte
|
||||||
|
|
||||||
|
// Decimal Separator
|
||||||
|
const (
|
||||||
|
DECIMAL_SEPARATOR_DEFAULT DecimalSeparator = 0
|
||||||
|
DECIMAL_SEPARATOR_DOT DecimalSeparator = 1
|
||||||
|
DECIMAL_SEPARATOR_COMMA DecimalSeparator = 2
|
||||||
|
DECIMAL_SEPARATOR_SPACE DecimalSeparator = 3
|
||||||
|
DECIMAL_SEPARATOR_INVALID DecimalSeparator = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a textual representation of the decimal separator enum
|
||||||
|
func (f DecimalSeparator) String() string {
|
||||||
|
switch f {
|
||||||
|
case DECIMAL_SEPARATOR_DEFAULT:
|
||||||
|
return "Default"
|
||||||
|
case DECIMAL_SEPARATOR_DOT:
|
||||||
|
return "Dot"
|
||||||
|
case DECIMAL_SEPARATOR_COMMA:
|
||||||
|
return "Comma"
|
||||||
|
case DECIMAL_SEPARATOR_SPACE:
|
||||||
|
return "Space"
|
||||||
|
case DECIMAL_SEPARATOR_INVALID:
|
||||||
|
return "Invalid"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Invalid(%d)", int(f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DigitGroupingSymbol represents the digit grouping symbol
|
||||||
|
type DigitGroupingSymbol byte
|
||||||
|
|
||||||
|
// Digit Grouping Symbol
|
||||||
|
const (
|
||||||
|
DIGIT_GROUPING_SYMBOL_DEFAULT DigitGroupingSymbol = 0
|
||||||
|
DIGIT_GROUPING_SYMBOL_DOT DigitGroupingSymbol = 1
|
||||||
|
DIGIT_GROUPING_SYMBOL_COMMA DigitGroupingSymbol = 2
|
||||||
|
DIGIT_GROUPING_SYMBOL_SPACE DigitGroupingSymbol = 3
|
||||||
|
DIGIT_GROUPING_SYMBOL_APOSTROPHE DigitGroupingSymbol = 4
|
||||||
|
DIGIT_GROUPING_SYMBOL_INVALID DigitGroupingSymbol = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a textual representation of the digit grouping symbol enum
|
||||||
|
func (f DigitGroupingSymbol) String() string {
|
||||||
|
switch f {
|
||||||
|
case DIGIT_GROUPING_SYMBOL_DEFAULT:
|
||||||
|
return "Default"
|
||||||
|
case DIGIT_GROUPING_SYMBOL_DOT:
|
||||||
|
return "Dot"
|
||||||
|
case DIGIT_GROUPING_SYMBOL_COMMA:
|
||||||
|
return "Comma"
|
||||||
|
case DIGIT_GROUPING_SYMBOL_SPACE:
|
||||||
|
return "Space"
|
||||||
|
case DIGIT_GROUPING_SYMBOL_APOSTROPHE:
|
||||||
|
return "Apostrophe"
|
||||||
|
case DIGIT_GROUPING_SYMBOL_INVALID:
|
||||||
|
return "Invalid"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Invalid(%d)", int(f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DigitGroupingType represents digit grouping type
|
||||||
|
type DigitGroupingType byte
|
||||||
|
|
||||||
|
// Digit Grouping Type
|
||||||
|
const (
|
||||||
|
DIGIT_GROUPING_TYPE_DEFAULT DigitGroupingType = 0
|
||||||
|
DIGIT_GROUPING_TYPE_NONE DigitGroupingType = 1
|
||||||
|
DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR DigitGroupingType = 2
|
||||||
|
DIGIT_GROUPING_TYPE_INVALID DigitGroupingType = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a textual representation of the digit grouping type enum
|
||||||
|
func (d DigitGroupingType) String() string {
|
||||||
|
switch d {
|
||||||
|
case DIGIT_GROUPING_TYPE_DEFAULT:
|
||||||
|
return "Default"
|
||||||
|
case DIGIT_GROUPING_TYPE_NONE:
|
||||||
|
return "None"
|
||||||
|
case DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR:
|
||||||
|
return "Thousands Separator"
|
||||||
|
case DIGIT_GROUPING_TYPE_INVALID:
|
||||||
|
return "Invalid"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Invalid(%d)", int(d))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/dgrijalva/jwt-go"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenType represents token type
|
// TokenType represents token type
|
||||||
@@ -9,14 +11,52 @@ type TokenType byte
|
|||||||
|
|
||||||
// Token types
|
// Token types
|
||||||
const (
|
const (
|
||||||
USER_TOKEN_TYPE_NORMAL TokenType = 1
|
USER_TOKEN_TYPE_NORMAL TokenType = 1
|
||||||
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
|
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
|
// UserTokenClaims represents user token
|
||||||
type UserTokenClaims struct {
|
type UserTokenClaims struct {
|
||||||
UserTokenId string `json:"userTokenId"`
|
UserTokenId string `json:"userTokenId"`
|
||||||
|
Uid int64 `json:"jti,string"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Type TokenType `json:"type"`
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
package datastore
|
package datastore
|
||||||
|
|
||||||
import "xorm.io/xorm"
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
// Database represents a database instance
|
// Database represents a database instance
|
||||||
type Database struct {
|
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
|
// DoTransaction runs a new database transaction
|
||||||
func (db *Database) DoTransaction(fn func(sess *xorm.Session) error) (err error) {
|
func (db *Database) DoTransaction(c *core.Context, fn func(sess *xorm.Session) error) (err error) {
|
||||||
sess := db.NewSession()
|
sess := db.engineGroup.NewSession()
|
||||||
|
|
||||||
|
if c != nil {
|
||||||
|
sess.Context(NewXOrmContextAdapter(c))
|
||||||
|
}
|
||||||
|
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
|
|
||||||
if err = sess.Begin(); err != nil {
|
if err = sess.Begin(); err != nil {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package datastore
|
|||||||
import (
|
import (
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"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
|
// Query returns a new database session in a specific database by sharding key
|
||||||
func (s *DataStore) Query(key int64) *xorm.Session {
|
func (s *DataStore) Query(c *core.Context, key int64) *xorm.Session {
|
||||||
return s.Choose(key).NewSession()
|
return s.Choose(key).NewSession(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoTransaction runs a new database transaction in a specific database by sharding key
|
// 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) {
|
func (s *DataStore) DoTransaction(key int64, c *core.Context, fn func(sess *xorm.Session) error) (err error) {
|
||||||
return s.Choose(key).DoTransaction(fn)
|
return s.Choose(key).DoTransaction(c, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncStructs updates database structs by database models
|
// SyncStructs updates database structs by database models
|
||||||
func (s *DataStore) SyncStructs(beans ...interface{}) error {
|
func (s *DataStore) SyncStructs(beans ...any) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for i := 0; i < len(s.databases); i++ {
|
for i := 0; i < len(s.databases); i++ {
|
||||||
err = s.databases[i].Sync2(beans...)
|
err = s.databases[i].engineGroup.Sync2(beans...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package datastore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -98,19 +99,19 @@ func initializeDatabase(dbConfig *settings.DatabaseConfig) (*Database, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
engineGroup.SetMaxIdleConns(dbConfig.MaxIdleConnection)
|
engineGroup.SetMaxIdleConns(int(dbConfig.MaxIdleConnection))
|
||||||
engineGroup.SetMaxOpenConns(dbConfig.MaxOpenConnection)
|
engineGroup.SetMaxOpenConns(int(dbConfig.MaxOpenConnection))
|
||||||
engineGroup.SetConnMaxLifetime(time.Duration(dbConfig.ConnectionMaxLifeTime) * time.Second)
|
engineGroup.SetConnMaxLifetime(time.Duration(dbConfig.ConnectionMaxLifeTime) * time.Second)
|
||||||
|
|
||||||
return &Database{
|
return &Database{
|
||||||
EngineGroup: engineGroup,
|
engineGroup: engineGroup,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDatabaseLogger(database *Database, config *settings.Config) {
|
func setDatabaseLogger(database *Database, config *settings.Config) {
|
||||||
if config.EnableQueryLog {
|
if config.EnableQueryLog {
|
||||||
database.SetLogger(NewXOrmLoggerAdapter(config.EnableQueryLog, config.LogLevel))
|
database.engineGroup.SetLogger(NewXOrmLoggerAdapter(config.EnableQueryLog, config.LogLevel))
|
||||||
database.ShowSQL(true)
|
database.engineGroup.ShowSQL(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,16 +127,12 @@ func getMysqlConnectionString(dbConfig *settings.DatabaseConfig) (string, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getPostgresConnectionString(dbConfig *settings.DatabaseConfig) (string, error) {
|
func getPostgresConnectionString(dbConfig *settings.DatabaseConfig) (string, error) {
|
||||||
host, port := "", ""
|
host, port, err := net.SplitHostPort(dbConfig.DatabaseHost)
|
||||||
fields := strings.Split(dbConfig.DatabaseHost, ":")
|
|
||||||
|
|
||||||
if len(fields) != 2 {
|
if err != nil {
|
||||||
return "", errs.ErrDatabaseHostInvalid
|
return "", errs.ErrDatabaseHostInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
host = strings.TrimSpace(fields[0])
|
|
||||||
port = strings.TrimSpace(fields[1])
|
|
||||||
|
|
||||||
if strings.HasPrefix(dbConfig.DatabaseHost, "/") { // unix socket path
|
if strings.HasPrefix(dbConfig.DatabaseHost, "/") { // unix socket path
|
||||||
return fmt.Sprintf("postgres://%s:%s@:%s/%s?sslmode=%s&host=%s",
|
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
|
url.QueryEscape(dbConfig.DatabaseUser), url.QueryEscape(dbConfig.DatabasePassword), port, dbConfig.DatabaseName, dbConfig.DatabaseSSLMode, host), nil
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
}
|
||||||
@@ -14,42 +14,42 @@ type XOrmLoggerAdapter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs debug log
|
// Debug logs debug log
|
||||||
func (logger XOrmLoggerAdapter) Debug(v ...interface{}) {
|
func (logger XOrmLoggerAdapter) Debug(v ...any) {
|
||||||
log.SqlQuery(v...)
|
log.SqlQuery(v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debugf logs debug log with custom format
|
// 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...)
|
log.SqlQueryf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs info log
|
// Info logs info log
|
||||||
func (logger XOrmLoggerAdapter) Info(v ...interface{}) {
|
func (logger XOrmLoggerAdapter) Info(v ...any) {
|
||||||
log.SqlQuery(v...)
|
log.SqlQuery(v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infof logs info log with custom format
|
// 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...)
|
log.SqlQueryf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs warn log
|
// Warn logs warn log
|
||||||
func (logger XOrmLoggerAdapter) Warn(v ...interface{}) {
|
func (logger XOrmLoggerAdapter) Warn(v ...any) {
|
||||||
log.SqlQuery(v...)
|
log.SqlQuery(v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnf logs warn log with custom format
|
// 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...)
|
log.SqlQueryf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs error log
|
// Error logs error log
|
||||||
func (logger XOrmLoggerAdapter) Error(v ...interface{}) {
|
func (logger XOrmLoggerAdapter) Error(v ...any) {
|
||||||
log.SqlQuery(v...)
|
log.SqlQuery(v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf logs error log with custom format
|
// 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...)
|
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
@@ -8,14 +8,15 @@ var (
|
|||||||
ErrAccountNotFound = NewNormalError(NormalSubcategoryAccount, 1, http.StatusBadRequest, "account not found")
|
ErrAccountNotFound = NewNormalError(NormalSubcategoryAccount, 1, http.StatusBadRequest, "account not found")
|
||||||
ErrAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 2, http.StatusBadRequest, "account type is invalid")
|
ErrAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 2, http.StatusBadRequest, "account type is invalid")
|
||||||
ErrAccountCurrencyInvalid = NewNormalError(NormalSubcategoryAccount, 3, http.StatusBadRequest, "account currency 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")
|
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")
|
ErrAccountCannotHaveSubAccounts = NewNormalError(NormalSubcategoryAccount, 5, http.StatusBadRequest, "account cannot have sub-accounts")
|
||||||
ErrParentAccountCannotSetCurrency = NewNormalError(NormalSubcategoryAccount, 6, http.StatusBadRequest, "parent account cannot set currency")
|
ErrParentAccountCannotSetCurrency = NewNormalError(NormalSubcategoryAccount, 6, http.StatusBadRequest, "parent account cannot set currency")
|
||||||
ErrParentAccountCannotSetBalance = NewNormalError(NormalSubcategoryAccount, 7, http.StatusBadRequest, "parent account cannot set balance")
|
ErrParentAccountCannotSetBalance = NewNormalError(NormalSubcategoryAccount, 7, http.StatusBadRequest, "parent account cannot set balance")
|
||||||
ErrSubAccountCategoryNotEqualsToParent = NewNormalError(NormalSubcategoryAccount, 8, http.StatusBadRequest, "sub account category not equals to parent")
|
ErrSubAccountCategoryNotEqualsToParent = NewNormalError(NormalSubcategoryAccount, 8, http.StatusBadRequest, "sub-account category not equals to parent")
|
||||||
ErrSubAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 9, http.StatusBadRequest, "sub account type invalid")
|
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")
|
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")
|
ErrSourceAccountNotFound = NewNormalError(NormalSubcategoryAccount, 11, http.StatusBadRequest, "source account not found")
|
||||||
ErrDestinationAccountNotFound = NewNormalError(NormalSubcategoryAccount, 12, http.StatusBadRequest, "destination 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")
|
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
@@ -1,7 +1,7 @@
|
|||||||
package errs
|
package errs
|
||||||
|
|
||||||
// ErrorCategory represents error category
|
// ErrorCategory represents error category
|
||||||
type ErrorCategory int
|
type ErrorCategory int32
|
||||||
|
|
||||||
// Error categories
|
// Error categories
|
||||||
const (
|
const (
|
||||||
@@ -14,6 +14,8 @@ const (
|
|||||||
SystemSubcategoryDefault = 0
|
SystemSubcategoryDefault = 0
|
||||||
SystemSubcategorySetting = 1
|
SystemSubcategorySetting = 1
|
||||||
SystemSubcategoryDatabase = 2
|
SystemSubcategoryDatabase = 2
|
||||||
|
SystemSubcategoryMail = 3
|
||||||
|
SystemSubcategoryLogging = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sub categories of normal error
|
// Sub categories of normal error
|
||||||
@@ -27,16 +29,19 @@ const (
|
|||||||
NormalSubcategoryCategory = 6
|
NormalSubcategoryCategory = 6
|
||||||
NormalSubcategoryTag = 7
|
NormalSubcategoryTag = 7
|
||||||
NormalSubcategoryDataManagement = 8
|
NormalSubcategoryDataManagement = 8
|
||||||
|
NormalSubcategoryMapProxy = 9
|
||||||
|
NormalSubcategoryTemplate = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents the specific error returned to user
|
// Error represents the specific error returned to user
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Category ErrorCategory
|
Category ErrorCategory
|
||||||
SubCategory int
|
SubCategory int32
|
||||||
Index int
|
Index int32
|
||||||
HttpStatusCode int
|
HttpStatusCode int
|
||||||
Message string
|
Message string
|
||||||
BaseError []error
|
BaseError []error
|
||||||
|
Context any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns the error message
|
// Error returns the error message
|
||||||
@@ -45,12 +50,12 @@ func (err *Error) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Code returns the error code
|
// Code returns the error code
|
||||||
func (err *Error) Code() int {
|
func (err *Error) Code() int32 {
|
||||||
return int(err.Category)*100000 + err.SubCategory*1000 + err.Index
|
return int32(err.Category)*100000 + err.SubCategory*1000 + err.Index
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new error instance
|
// 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{
|
return &Error{
|
||||||
Category: category,
|
Category: category,
|
||||||
SubCategory: subCategory,
|
SubCategory: subCategory,
|
||||||
@@ -62,15 +67,24 @@ func New(category ErrorCategory, subCategory int, index int, httpStatusCode int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSystemError returns a new system error instance
|
// 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)
|
return New(CATEGORY_SYSTEM, subCategory, index, httpStatusCode, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNormalError returns a new normal error instance
|
// 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)
|
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
|
// NewIncompleteOrIncorrectSubmissionError returns a new incomplete or incorrect submission error instance
|
||||||
func NewIncompleteOrIncorrectSubmissionError(err error) *Error {
|
func NewIncompleteOrIncorrectSubmissionError(err error) *Error {
|
||||||
return New(ErrIncompleteOrIncorrectSubmission.Category,
|
return New(ErrIncompleteOrIncorrectSubmission.Category,
|
||||||
@@ -80,6 +94,19 @@ func NewIncompleteOrIncorrectSubmissionError(err error) *Error {
|
|||||||
ErrIncompleteOrIncorrectSubmission.Message, err)
|
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 would return the error from err parameter if the this error is defined in this project,
|
||||||
// or return the default error
|
// or return the default error
|
||||||
func Or(err error, defaultErr *Error) *Error {
|
func Or(err error, defaultErr *Error) *Error {
|
||||||
|
|||||||
+6
-1
@@ -16,7 +16,7 @@ var (
|
|||||||
ErrPageIndexInvalid = NewNormalError(NormalSubcategoryGlobal, 6, http.StatusBadRequest, "page index is invalid")
|
ErrPageIndexInvalid = NewNormalError(NormalSubcategoryGlobal, 6, http.StatusBadRequest, "page index is invalid")
|
||||||
ErrPageCountInvalid = NewNormalError(NormalSubcategoryGlobal, 7, http.StatusBadRequest, "page count is invalid")
|
ErrPageCountInvalid = NewNormalError(NormalSubcategoryGlobal, 7, http.StatusBadRequest, "page count is invalid")
|
||||||
ErrClientTimezoneOffsetInvalid = NewNormalError(NormalSubcategoryGlobal, 8, http.StatusBadRequest, "client timezone offset 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")
|
ErrQueryItemsTooMuch = NewNormalError(NormalSubcategoryGlobal, 10, http.StatusBadRequest, "query items too much")
|
||||||
ErrQueryItemsInvalid = NewNormalError(NormalSubcategoryGlobal, 11, http.StatusBadRequest, "query items have invalid item")
|
ErrQueryItemsInvalid = NewNormalError(NormalSubcategoryGlobal, 11, http.StatusBadRequest, "query items have invalid item")
|
||||||
ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid")
|
ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid")
|
||||||
@@ -82,3 +82,8 @@ func GetParameterInvalidCurrencyMessage(field string) string {
|
|||||||
func GetParameterInvalidHexRGBColorMessage(field string) string {
|
func GetParameterInvalidHexRGBColorMessage(field string) string {
|
||||||
return fmt.Sprintf("parameter \"%s\" is invalid color", field)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package errs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error codes related to logging
|
||||||
|
var (
|
||||||
|
ErrLoggingError = NewSystemError(SystemSubcategoryLogging, 0, http.StatusInternalServerError, "logging error")
|
||||||
|
)
|
||||||
@@ -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")
|
||||||
|
)
|
||||||
@@ -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
@@ -4,9 +4,23 @@ import "net/http"
|
|||||||
|
|
||||||
// Error codes related to settings
|
// Error codes related to settings
|
||||||
var (
|
var (
|
||||||
ErrInvalidProtocol = NewSystemError(SystemSubcategorySetting, 0, http.StatusInternalServerError, "invalid server protocol")
|
ErrInvalidServerMode = NewSystemError(SystemSubcategorySetting, 0, http.StatusInternalServerError, "invalid server mode")
|
||||||
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 1, http.StatusInternalServerError, "invalid log mode")
|
ErrInvalidProtocol = NewSystemError(SystemSubcategorySetting, 1, http.StatusInternalServerError, "invalid server protocol")
|
||||||
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "failed to get local address")
|
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "invalid log mode")
|
||||||
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid uuid mode")
|
ErrInvalidLogLevel = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid log level")
|
||||||
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "invalid exchange rates data source")
|
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
@@ -4,8 +4,11 @@ import "net/http"
|
|||||||
|
|
||||||
// Error codes related to transaction categories
|
// Error codes related to transaction categories
|
||||||
var (
|
var (
|
||||||
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
|
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
|
||||||
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
|
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
|
||||||
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
|
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
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
@@ -6,16 +6,19 @@ import (
|
|||||||
|
|
||||||
// Error codes related to tokens
|
// Error codes related to tokens
|
||||||
var (
|
var (
|
||||||
ErrTokenGenerating = NewNormalError(NormalSubcategoryToken, 0, http.StatusInternalServerError, "failed to generate token")
|
ErrTokenGenerating = NewNormalError(NormalSubcategoryToken, 0, http.StatusInternalServerError, "failed to generate token")
|
||||||
ErrUnauthorizedAccess = NewNormalError(NormalSubcategoryToken, 1, http.StatusUnauthorized, "unauthorized access")
|
ErrUnauthorizedAccess = NewNormalError(NormalSubcategoryToken, 1, http.StatusUnauthorized, "unauthorized access")
|
||||||
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
|
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
|
||||||
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
|
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
|
||||||
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
|
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
|
||||||
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two factor authorization")
|
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two-factor authorization")
|
||||||
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two factor authorization")
|
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two-factor authorization")
|
||||||
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
|
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
|
||||||
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
|
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
|
||||||
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user 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")
|
ErrTokenRecordNotFound = NewNormalError(NormalSubcategoryToken, 10, http.StatusBadRequest, "token is not found")
|
||||||
ErrTokenExpired = NewNormalError(NormalSubcategoryToken, 11, http.StatusBadRequest, "token is expired")
|
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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ var (
|
|||||||
ErrCannotAddTransactionToHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 11, http.StatusBadRequest, "cannot add transaction to hidden account")
|
ErrCannotAddTransactionToHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 11, http.StatusBadRequest, "cannot add transaction to hidden account")
|
||||||
ErrCannotModifyTransactionInHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 12, http.StatusBadRequest, "cannot modify transaction of 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")
|
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")
|
ErrCannotAddTransactionToParentAccount = NewNormalError(NormalSubcategoryTransaction, 14, http.StatusBadRequest, "cannot add transaction to parent account")
|
||||||
ErrCannotModifyTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 15, http.StatusBadRequest, "cannot modify transaction with this transaction time")
|
ErrCannotModifyTransactionInParentAccount = NewNormalError(NormalSubcategoryTransaction, 15, http.StatusBadRequest, "cannot modify transaction of parent account")
|
||||||
ErrCannotDeleteTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 16, http.StatusBadRequest, "cannot delete transaction with this transaction time")
|
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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import "net/http"
|
|||||||
|
|
||||||
// Error codes related to transaction categories
|
// Error codes related to transaction categories
|
||||||
var (
|
var (
|
||||||
ErrTransactionCategoryIdInvalid = NewNormalError(NormalSubcategoryCategory, 0, http.StatusBadRequest, "transaction category id is invalid")
|
ErrTransactionCategoryIdInvalid = NewNormalError(NormalSubcategoryCategory, 0, http.StatusBadRequest, "transaction category id is invalid")
|
||||||
ErrTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 1, http.StatusBadRequest, "transaction category not found")
|
ErrTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 1, http.StatusBadRequest, "transaction category not found")
|
||||||
ErrTransactionCategoryTypeInvalid = NewNormalError(NormalSubcategoryCategory, 2, http.StatusBadRequest, "transaction category type is invalid")
|
ErrTransactionCategoryTypeInvalid = NewNormalError(NormalSubcategoryCategory, 2, http.StatusBadRequest, "transaction category type is invalid")
|
||||||
ErrParentTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 3, http.StatusBadRequest, "parent transaction category not found")
|
ErrParentTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 3, http.StatusBadRequest, "parent transaction category not found")
|
||||||
ErrCannotAddToSecondaryTransactionCategory = NewNormalError(NormalSubcategoryCategory, 4, http.StatusBadRequest, "cannot add to secondary transaction category")
|
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")
|
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")
|
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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ var (
|
|||||||
ErrTransactionTagNameIsEmpty = NewNormalError(NormalSubcategoryTag, 2, http.StatusBadRequest, "transaction tag name is empty")
|
ErrTransactionTagNameIsEmpty = NewNormalError(NormalSubcategoryTag, 2, http.StatusBadRequest, "transaction tag name is empty")
|
||||||
ErrTransactionTagNameAlreadyExists = NewNormalError(NormalSubcategoryTag, 3, http.StatusBadRequest, "transaction tag name already exists")
|
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")
|
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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
)
|
||||||
@@ -2,11 +2,11 @@ package errs
|
|||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
// Error codes related to two factor authorization
|
// Error codes related to two-factor authorization
|
||||||
var (
|
var (
|
||||||
ErrPasscodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 0, http.StatusUnauthorized, "passcode is invalid")
|
ErrPasscodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 0, http.StatusUnauthorized, "passcode is invalid")
|
||||||
ErrTwoFactorRecoveryCodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 1, http.StatusUnauthorized, "two factor backup code 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")
|
ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NormalSubcategoryTwofactor, 2, http.StatusUnauthorized, "two-factor backup code does not exist")
|
||||||
ErrTwoFactorIsNotEnabled = NewNormalError(NormalSubcategoryTwofactor, 3, http.StatusBadRequest, "two factor is not enabled")
|
ErrTwoFactorIsNotEnabled = NewNormalError(NormalSubcategoryTwofactor, 3, http.StatusBadRequest, "two-factor is not enabled")
|
||||||
ErrTwoFactorAlreadyEnabled = NewNormalError(NormalSubcategoryTwofactor, 4, http.StatusBadRequest, "two factor has already been enabled")
|
ErrTwoFactorAlreadyEnabled = NewNormalError(NormalSubcategoryTwofactor, 4, http.StatusBadRequest, "two-factor has already been enabled")
|
||||||
)
|
)
|
||||||
|
|||||||
+28
-15
@@ -6,19 +6,32 @@ import (
|
|||||||
|
|
||||||
// Error codes related to users
|
// Error codes related to users
|
||||||
var (
|
var (
|
||||||
ErrLoginNameInvalid = NewNormalError(NormalSubcategoryUser, 0, http.StatusUnauthorized, "login name is invalid")
|
ErrLoginNameInvalid = NewNormalError(NormalSubcategoryUser, 0, http.StatusUnauthorized, "login name is invalid")
|
||||||
ErrLoginNameOrPasswordInvalid = NewNormalError(NormalSubcategoryUser, 1, http.StatusUnauthorized, "login name or password 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")
|
ErrLoginNameOrPasswordWrong = NewNormalError(NormalSubcategoryUser, 2, http.StatusUnauthorized, "login name or password is wrong")
|
||||||
ErrUserIdInvalid = NewNormalError(NormalSubcategoryUser, 3, http.StatusBadRequest, "user id is invalid")
|
ErrUserIdInvalid = NewNormalError(NormalSubcategoryUser, 3, http.StatusBadRequest, "user id is invalid")
|
||||||
ErrUsernameIsEmpty = NewNormalError(NormalSubcategoryUser, 4, http.StatusBadRequest, "username is empty")
|
ErrUsernameIsEmpty = NewNormalError(NormalSubcategoryUser, 4, http.StatusBadRequest, "username is empty")
|
||||||
ErrEmailIsEmpty = NewNormalError(NormalSubcategoryUser, 5, http.StatusBadRequest, "email is empty")
|
ErrEmailIsEmpty = NewNormalError(NormalSubcategoryUser, 5, http.StatusBadRequest, "email is empty")
|
||||||
ErrNicknameIsEmpty = NewNormalError(NormalSubcategoryUser, 6, http.StatusBadRequest, "nickname is empty")
|
ErrNicknameIsEmpty = NewNormalError(NormalSubcategoryUser, 6, http.StatusBadRequest, "nickname is empty")
|
||||||
ErrPasswordIsEmpty = NewNormalError(NormalSubcategoryUser, 7, http.StatusBadRequest, "password is empty")
|
ErrPasswordIsEmpty = NewNormalError(NormalSubcategoryUser, 7, http.StatusBadRequest, "password is empty")
|
||||||
ErrUserDefaultCurrencyIsEmpty = NewNormalError(NormalSubcategoryUser, 8, http.StatusBadRequest, "user default currency is empty")
|
ErrUserDefaultCurrencyIsEmpty = NewNormalError(NormalSubcategoryUser, 8, http.StatusBadRequest, "user default currency is empty")
|
||||||
ErrUserDefaultCurrencyIsInvalid = NewNormalError(NormalSubcategoryUser, 9, http.StatusBadRequest, "user default currency is invalid")
|
ErrUserDefaultCurrencyIsInvalid = NewNormalError(NormalSubcategoryUser, 9, http.StatusBadRequest, "user default currency is invalid")
|
||||||
ErrUserNotFound = NewNormalError(NormalSubcategoryUser, 10, http.StatusBadRequest, "user not found")
|
ErrUserNotFound = NewNormalError(NormalSubcategoryUser, 10, http.StatusBadRequest, "user not found")
|
||||||
ErrUserPasswordWrong = NewNormalError(NormalSubcategoryUser, 11, http.StatusBadRequest, "password is wrong")
|
ErrUserPasswordWrong = NewNormalError(NormalSubcategoryUser, 11, http.StatusBadRequest, "password is wrong")
|
||||||
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
|
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
|
||||||
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
|
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
|
||||||
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
|
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
|
// 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
|
// ToLatestExchangeRateResponse returns a view-object according to original data from bank of Canada
|
||||||
func (e *BankOfCanadaExchangeRateData) ToLatestExchangeRateResponse(c *core.Context) *models.LatestExchangeRateResponse {
|
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)
|
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"]
|
exchangeRate := data["v"]
|
||||||
|
|
||||||
if exchangeRateValue, ok2 := exchangeRate.(string); ok2 {
|
if exchangeRateValue, ok2 := exchangeRate.(string); ok2 {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const euroCentralBankDataSource = "European Central Bank"
|
|||||||
const euroCentralBankBaseCurrency = "EUR"
|
const euroCentralBankBaseCurrency = "EUR"
|
||||||
|
|
||||||
const euroCentralBankDataUpdateDateFormat = "2006-01-02 15"
|
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
|
// EuroCentralBankDataSource defines the structure of exchange rates data source of euro central bank
|
||||||
type EuroCentralBankDataSource struct {
|
type EuroCentralBankDataSource struct {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error {
|
|||||||
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
|
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
|
||||||
Container.Current = &NationalBankOfPolandDataSource{}
|
Container.Current = &NationalBankOfPolandDataSource{}
|
||||||
return nil
|
return nil
|
||||||
|
} else if config.ExchangeRatesDataSource == settings.MonetaryAuthorityOfSingaporeDataSource {
|
||||||
|
Container.Current = &MonetaryAuthorityOfSingaporeDataSource{}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errs.ErrInvalidExchangeRatesDataSource
|
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"
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
const nationalBankOfPolandDailyExchangeRateUrl = "https://www.nbp.pl/kursy/xml/en/lastaen.xml"
|
const nationalBankOfPolandDailyExchangeRateUrl = "https://api.nbp.pl/api/exchangerates/tables/A?format=xml"
|
||||||
const nationalBankOfPolandInconvertibleCurrencyExchangeRateUrl = "https://www.nbp.pl/kursy/xml/en/lastben.xml"
|
const nationalBankOfPolandInconvertibleCurrencyExchangeRateUrl = "https://api.nbp.pl/api/exchangerates/tables/B?format=xml"
|
||||||
const nationalBankOfPolandExchangeRateReferenceUrl = "https://www.nbp.pl/homen.aspx?f=/kursy/kursyen.htm"
|
const nationalBankOfPolandExchangeRateReferenceUrl = "https://nbp.pl/en/statistic-and-financial-reporting/rates/"
|
||||||
const nationalBankOfPolandDataSource = "Narodowy Bank Polski"
|
const nationalBankOfPolandDataSource = "Narodowy Bank Polski"
|
||||||
const nationalBankOfPolandBaseCurrency = "PLN"
|
const nationalBankOfPolandBaseCurrency = "PLN"
|
||||||
|
|
||||||
@@ -30,16 +30,15 @@ type NationalBankOfPolandDataSource struct {
|
|||||||
|
|
||||||
// NationalBankOfPolandExchangeRateData represents the whole data from National Bank of Poland
|
// NationalBankOfPolandExchangeRateData represents the whole data from National Bank of Poland
|
||||||
type NationalBankOfPolandExchangeRateData struct {
|
type NationalBankOfPolandExchangeRateData struct {
|
||||||
XMLName xml.Name `xml:"exchange_rates"`
|
XMLName xml.Name `xml:"ArrayOfExchangeRatesTable"`
|
||||||
Date string `xml:"date,attr"`
|
Date string `xml:"ExchangeRatesTable>EffectiveDate"`
|
||||||
AllExchangeRates []*NationalBankOfPolandExchangeRate `xml:"mid-rate"`
|
AllExchangeRates []*NationalBankOfPolandExchangeRate `xml:"ExchangeRatesTable>Rates>Rate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NationalBankOfPolandExchangeRate represents the exchange rate data from National Bank of Poland
|
// NationalBankOfPolandExchangeRate represents the exchange rate data from National Bank of Poland
|
||||||
type NationalBankOfPolandExchangeRate struct {
|
type NationalBankOfPolandExchangeRate struct {
|
||||||
Currency string `xml:"code,attr"`
|
Currency string `xml:"Code"`
|
||||||
Units string `xml:"units,attr"`
|
Rate string `xml:"Mid"`
|
||||||
Rate string `xml:",chardata"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToLatestExchangeRateResponse returns a view-object according to original data from National Bank of Poland
|
// 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
|
// ToLatestExchangeRate returns a data pair according to original data from National Bank of Poland
|
||||||
func (e *NationalBankOfPolandExchangeRate) ToLatestExchangeRate(c *core.Context) *models.LatestExchangeRate {
|
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)
|
rate, err := utils.StringToFloat64(e.Rate)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,7 +106,7 @@ func (e *NationalBankOfPolandExchangeRate) ToLatestExchangeRate(c *core.Context)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
finalRate := float64(amount) / rate
|
finalRate := 1 / rate
|
||||||
|
|
||||||
if math.IsInf(finalRate, 0) {
|
if math.IsInf(finalRate, 0) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,11 +10,22 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const nationalBankOfPolandMinimumRequiredContent = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" +
|
const nationalBankOfPolandMinimumRequiredContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
|
||||||
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n" +
|
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
|
||||||
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\">3.8986</mid-rate>\n" +
|
" <ExchangeRatesTable>\n" +
|
||||||
" <mid-rate currency=\"Yuan Renminbi\" units=\"1\" code=\"CNY\">0.5941</mid-rate>\n" +
|
" <EffectiveDate>2024-02-28</EffectiveDate>\n" +
|
||||||
"</exchange_rates>"
|
" <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) {
|
func TestNationalBankOfPolandDataSource_StandardDataExtractBaseCurrency(t *testing.T) {
|
||||||
dataSource := &NationalBankOfPolandDataSource{}
|
dataSource := &NationalBankOfPolandDataSource{}
|
||||||
@@ -37,11 +48,11 @@ func TestNationalBankOfPolandDataSource_StandardDataExtractExchangeRates(t *test
|
|||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
|
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
|
||||||
Currency: "USD",
|
Currency: "USD",
|
||||||
Rate: "0.25650233417124096",
|
Rate: "0.2504884524823406",
|
||||||
})
|
})
|
||||||
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
|
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
|
||||||
Currency: "CNY",
|
Currency: "CNY",
|
||||||
Rate: "1.68321831341525",
|
Rate: "1.8034265103697025",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +72,33 @@ func TestNationalBankOfPolandDataSource_OnlyXMLHeader(t *testing.T) {
|
|||||||
Context: &gin.Context{},
|
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)
|
assert.NotEqual(t, nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +108,14 @@ func TestNationalBankOfPolandDataSource_EmptyExchangeRatesContent(t *testing.T)
|
|||||||
Context: &gin.Context{},
|
Context: &gin.Context{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
|
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
|
||||||
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
|
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
|
||||||
"</exchange_rates>"))
|
" <ExchangeRatesTable>\n"+
|
||||||
|
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
|
||||||
|
" <Rates>\n"+
|
||||||
|
" </Rates>\n"+
|
||||||
|
" </ExchangeRatesTable>\n"+
|
||||||
|
"</ArrayOfExchangeRatesTable>"))
|
||||||
assert.NotEqual(t, nil, err)
|
assert.NotEqual(t, nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +125,18 @@ func TestNationalBankOfPolandDataSource_InvalidCurrency(t *testing.T) {
|
|||||||
Context: &gin.Context{},
|
Context: &gin.Context{},
|
||||||
}
|
}
|
||||||
|
|
||||||
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
|
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
|
||||||
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
|
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
|
||||||
" <mid-rate currency=\"XXX\" units=\"1\" code=\"XXX\">1</mid-rate>\n"+
|
" <ExchangeRatesTable>\n"+
|
||||||
"</exchange_rates>"))
|
" <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.Equal(t, nil, err)
|
||||||
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
|
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
|
||||||
}
|
}
|
||||||
@@ -97,10 +147,18 @@ func TestNationalBankOfPolandDataSource_EmptyRate(t *testing.T) {
|
|||||||
Context: &gin.Context{},
|
Context: &gin.Context{},
|
||||||
}
|
}
|
||||||
|
|
||||||
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
|
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
|
||||||
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
|
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
|
||||||
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\"></mid-rate>\n"+
|
" <ExchangeRatesTable>\n"+
|
||||||
"</exchange_rates>"))
|
" <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.Equal(t, nil, err)
|
||||||
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
|
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
|
||||||
}
|
}
|
||||||
@@ -111,10 +169,18 @@ func TestNationalBankOfPolandDataSource_InvalidRate(t *testing.T) {
|
|||||||
Context: &gin.Context{},
|
Context: &gin.Context{},
|
||||||
}
|
}
|
||||||
|
|
||||||
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
|
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
|
||||||
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
|
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
|
||||||
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\">null</mid-rate>\n"+
|
" <ExchangeRatesTable>\n"+
|
||||||
"</exchange_rates>"))
|
" <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.Equal(t, nil, err)
|
||||||
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
|
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package locales
|
||||||
|
|
||||||
|
// LocaleInfo represents locale info
|
||||||
|
type LocaleInfo struct {
|
||||||
|
Aliases []string
|
||||||
|
Content *LocaleTextItems
|
||||||
|
}
|
||||||
@@ -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 分钟后过期。",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -41,12 +41,12 @@ func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|||||||
b.WriteString("] ")
|
b.WriteString("] ")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(entry.Message)
|
|
||||||
|
|
||||||
if requestId, exists := entry.Data[logFieldRequestId]; exists {
|
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")
|
b.WriteString("\n")
|
||||||
|
|
||||||
if extra, exists := entry.Data[logFieldExtra]; exists {
|
if extra, exists := entry.Data[logFieldExtra]; exists {
|
||||||
|
|||||||
+80
-41
@@ -39,34 +39,73 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetLoggerConfiguration sets the logger according to the config
|
// 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 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 {
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bootWriters = append(bootWriters, logFile)
|
if !isDisableBootLog {
|
||||||
writers = append(writers, logFile)
|
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...)
|
bootMultipleWriter := io.MultiWriter(bootWriters...)
|
||||||
multipleWriter := io.MultiWriter(writers...)
|
defaultMultipleWriter := io.MultiWriter(defaultWriters...)
|
||||||
|
requestMultipleWriter := io.MultiWriter(requestWriters...)
|
||||||
|
queryMultipleWriter := io.MultiWriter(queryWriters...)
|
||||||
|
|
||||||
bootLogger.SetOutput(bootMultipleWriter)
|
bootLogger.SetOutput(bootMultipleWriter)
|
||||||
defaultLogger.SetOutput(multipleWriter)
|
defaultLogger.SetOutput(defaultMultipleWriter)
|
||||||
requestLogger.SetOutput(multipleWriter)
|
requestLogger.SetOutput(requestMultipleWriter)
|
||||||
sqlQueryLogger.SetOutput(multipleWriter)
|
sqlQueryLogger.SetOutput(queryMultipleWriter)
|
||||||
|
|
||||||
if config.LogLevel == settings.LOGLEVEL_DEBUG {
|
if config.LogLevel == settings.LOGLEVEL_DEBUG {
|
||||||
bootLogger.SetLevel(logrus.DebugLevel)
|
bootLogger.SetLevel(logrus.DebugLevel)
|
||||||
@@ -94,93 +133,93 @@ func SetLoggerConfiguration(config *settings.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Debugf logs debug log with custom format
|
// Debugf logs debug log with custom format
|
||||||
func Debugf(format string, args ...interface{}) {
|
func Debugf(format string, args ...any) {
|
||||||
defaultLogger.Debugf(getFinalLog(format, args...))
|
defaultLogger.Debug(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DebugfWithRequestId logs debug log with custom format and request id
|
// DebugfWithRequestId logs debug log with custom format and request id
|
||||||
func DebugfWithRequestId(c *core.Context, format string, args ...interface{}) {
|
func DebugfWithRequestId(c *core.Context, format string, args ...any) {
|
||||||
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Debugf(getFinalLog(format, args...))
|
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Debug(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infof logs info log with custom format
|
// Infof logs info log with custom format
|
||||||
func Infof(format string, args ...interface{}) {
|
func Infof(format string, args ...any) {
|
||||||
defaultLogger.Infof(getFinalLog(format, args...))
|
defaultLogger.Info(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// InfofWithRequestId logs info log with custom format and request id
|
// InfofWithRequestId logs info log with custom format and request id
|
||||||
func InfofWithRequestId(c *core.Context, format string, args ...interface{}) {
|
func InfofWithRequestId(c *core.Context, format string, args ...any) {
|
||||||
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Infof(getFinalLog(format, args...))
|
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Info(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnf logs warn log with custom format
|
// Warnf logs warn log with custom format
|
||||||
func Warnf(format string, args ...interface{}) {
|
func Warnf(format string, args ...any) {
|
||||||
defaultLogger.Warnf(getFinalLog(format, args...))
|
defaultLogger.Warn(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// WarnfWithRequestId logs warn log with custom format and request id
|
// WarnfWithRequestId logs warn log with custom format and request id
|
||||||
func WarnfWithRequestId(c *core.Context, format string, args ...interface{}) {
|
func WarnfWithRequestId(c *core.Context, format string, args ...any) {
|
||||||
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Warnf(getFinalLog(format, args...))
|
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Warn(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf logs error log with custom format
|
// Errorf logs error log with custom format
|
||||||
func Errorf(format string, args ...interface{}) {
|
func Errorf(format string, args ...any) {
|
||||||
defaultLogger.Errorf(getFinalLog(format, args...))
|
defaultLogger.Error(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorfWithRequestId logs error log with custom format and request id
|
// ErrorfWithRequestId logs error log with custom format and request id
|
||||||
func ErrorfWithRequestId(c *core.Context, format string, args ...interface{}) {
|
func ErrorfWithRequestId(c *core.Context, format string, args ...any) {
|
||||||
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Errorf(getFinalLog(format, args...))
|
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).Error(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorfWithRequestIdAndExtra logs error log with custom format and request id and extra info
|
// ErrorfWithRequestIdAndExtra logs error log with custom format and request id and extra info
|
||||||
func ErrorfWithRequestIdAndExtra(c *core.Context, extraString string, format string, args ...interface{}) {
|
func ErrorfWithRequestIdAndExtra(c *core.Context, extraString string, format string, args ...any) {
|
||||||
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).WithField(logFieldExtra, extraString).Errorf(getFinalLog(format, args...))
|
defaultLogger.WithField(logFieldRequestId, c.GetRequestId()).WithField(logFieldExtra, extraString).Error(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// BootInfof logs boot info log
|
// BootInfof logs boot info log
|
||||||
func BootInfof(format string, args ...interface{}) {
|
func BootInfof(format string, args ...any) {
|
||||||
if bootLogger != nil {
|
if bootLogger != nil {
|
||||||
bootLogger.Infof(getFinalLog(format, args...))
|
bootLogger.Info(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BootWarnf logs boot warn log
|
// BootWarnf logs boot warn log
|
||||||
func BootWarnf(format string, args ...interface{}) {
|
func BootWarnf(format string, args ...any) {
|
||||||
if bootLogger != nil {
|
if bootLogger != nil {
|
||||||
bootLogger.Warnf(getFinalLog(format, args...))
|
bootLogger.Warn(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BootErrorf logs boot error log
|
// BootErrorf logs boot error log
|
||||||
func BootErrorf(format string, args ...interface{}) {
|
func BootErrorf(format string, args ...any) {
|
||||||
if bootLogger != nil {
|
if bootLogger != nil {
|
||||||
bootLogger.Errorf(getFinalLog(format, args...))
|
bootLogger.Error(getFinalLog(format, args...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requestf logs http request log with custom format
|
// 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 {
|
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
|
// SqlQuery logs sql query log
|
||||||
func SqlQuery(args ...interface{}) {
|
func SqlQuery(args ...any) {
|
||||||
if sqlQueryLogger != nil {
|
if sqlQueryLogger != nil {
|
||||||
sqlQueryLogger.Info(args...)
|
sqlQueryLogger.Info(args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SqlQueryf logs sql query log with custom format
|
// SqlQueryf logs sql query log with custom format
|
||||||
func SqlQueryf(format string, args ...interface{}) {
|
func SqlQueryf(format string, args ...any) {
|
||||||
if sqlQueryLogger != nil {
|
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 := fmt.Sprintf(format, args...)
|
||||||
result = strings.Replace(result, "\n", " ", -1)
|
result = strings.Replace(result, "\n", " ", -1)
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
// MailMessage represents an email entity
|
||||||
|
type MailMessage struct {
|
||||||
|
To string
|
||||||
|
Subject string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
// Mailer is email sender interface
|
||||||
|
type Mailer interface {
|
||||||
|
SendMail(message *MailMessage) error
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package middlewares
|
package middlewares
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
@@ -10,51 +10,36 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"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"
|
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) {
|
func JWTAuthorization(c *core.Context) {
|
||||||
claims, err := getTokenClaims(c)
|
jwtAuthorization(c, TOKEN_SOURCE_TYPE_HEADER)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
utils.PrintJsonErrorResult(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
|
|
||||||
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func JWTAuthorizationByQueryString(c *core.Context) {
|
||||||
token, exists := c.GetQuery(tokenQueryStringParam)
|
jwtAuthorization(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
||||||
|
}
|
||||||
|
|
||||||
if !exists {
|
// JWTAuthorizationByCookie verifies whether current request is valid by jwt token in cookie
|
||||||
log.WarnfWithRequestId(c, "[authorization.JWTAuthorizationByQueryString] no token provided")
|
func JWTAuthorizationByCookie(c *core.Context) {
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrUnauthorizedAccess)
|
jwtAuthorization(c, TOKEN_SOURCE_TYPE_COOKIE)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Request.Header.Set("Authorization", token)
|
|
||||||
|
|
||||||
JWTAuthorization(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
|
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
|
||||||
func JWTTwoFactorAuthorization(c *core.Context) {
|
func JWTTwoFactorAuthorization(c *core.Context) {
|
||||||
claims, err := getTokenClaims(c)
|
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.PrintJsonErrorResult(c, err)
|
utils.PrintJsonErrorResult(c, err)
|
||||||
@@ -62,7 +47,7 @@ func JWTTwoFactorAuthorization(c *core.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
|
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)
|
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenNotRequire2FA)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -71,12 +56,74 @@ func JWTTwoFactorAuthorization(c *core.Context) {
|
|||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
|
// JWTEmailVerifyAuthorization verifies whether current request is email verification
|
||||||
token, claims, err := services.Tokens.ParseToken(c)
|
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 {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] failed to parse token, because %s", err.Error())
|
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 {
|
if !token.Valid {
|
||||||
@@ -84,15 +131,20 @@ func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
|
|||||||
return nil, errs.ErrCurrentInvalidToken
|
return nil, errs.ErrCurrentInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
|
if claims.Uid <= 0 {
|
||||||
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is expired")
|
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is invalid")
|
||||||
return nil, errs.ErrCurrentTokenExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
if claims.Id == "" {
|
|
||||||
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is empty")
|
|
||||||
return nil, errs.ErrCurrentInvalidToken
|
return nil, errs.ErrCurrentInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims, nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package middlewares
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"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)
|
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||||
|
|
||||||
if file != lastFile {
|
if file != lastFile {
|
||||||
data, err := ioutil.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func RequestId(config *settings.Config) core.MiddlewareHandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
requestId := requestid.Container.Current.GenerateRequestId(c.ClientIP())
|
requestId := requestid.Container.Current.GenerateRequestId(c.ClientIP(), c.ClientPort())
|
||||||
c.SetRequestId(requestId)
|
c.SetRequestId(requestId)
|
||||||
|
|
||||||
if config.EnableRequestIdHeader {
|
if config.EnableRequestIdHeader {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequestLog logs the http request log
|
// RequestLog logs the http request log
|
||||||
@@ -18,7 +19,7 @@ func RequestLog(c *core.Context) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
statusCode := c.Writer.Status()
|
statusCode := c.Writer.Status()
|
||||||
errorCode := 0
|
errorCode := int32(0)
|
||||||
|
|
||||||
userId := "-"
|
userId := "-"
|
||||||
claims := c.GetTokenClaims()
|
claims := c.GetTokenClaims()
|
||||||
@@ -28,7 +29,7 @@ func RequestLog(c *core.Context) {
|
|||||||
method := c.Request.Method
|
method := c.Request.Method
|
||||||
|
|
||||||
if claims != nil {
|
if claims != nil {
|
||||||
userId = claims.Id
|
userId = utils.Int64ToString(claims.Uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package middlewares
|
package middlewares
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
@@ -15,16 +17,90 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
|
|||||||
return func(c *core.Context) {
|
return func(c *core.Context) {
|
||||||
settingsArr := []string{
|
settingsArr := []string{
|
||||||
buildBooleanSetting("r", config.EnableUserRegister),
|
buildBooleanSetting("r", config.EnableUserRegister),
|
||||||
|
buildBooleanSetting("f", config.EnableUserForgetPassword),
|
||||||
|
buildBooleanSetting("v", config.EnableUserVerifyEmail),
|
||||||
buildBooleanSetting("e", config.EnableDataExport),
|
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, "_")
|
bundledSettings := strings.Join(settingsArr, "_")
|
||||||
c.SetCookie(settingsCookieName, bundledSettings, config.TokenExpiredTime, "", "", false, false)
|
c.SetCookie(settingsCookieName, bundledSettings, int(config.TokenExpiredTime), "", "", false, false)
|
||||||
|
|
||||||
c.Next()
|
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 {
|
func buildBooleanSetting(key string, value bool) string {
|
||||||
if value {
|
if value {
|
||||||
return fmt.Sprintf("%s.1", key)
|
return fmt.Sprintf("%s.1", key)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user