Compare commits
1154 Commits
Author | SHA1 | Date |
---|---|---|
Derrick Hammer | 1b394f3ca1 | |
Derrick Hammer | 8d98f131d5 | |
Derrick Hammer | ba67d19299 | |
Derrick Hammer | d5575fbf4f | |
Derrick Hammer | 86669d9f08 | |
Derrick Hammer | 818ee60aea | |
Derrick Hammer | d913e0f7b2 | |
Derrick Hammer | 8ccd90825b | |
Derrick Hammer | 09c9ab8614 | |
Derrick Hammer | 8e9d2d0398 | |
Derrick Hammer | aeba225c87 | |
Derrick Hammer | 5ad1aaeb6a | |
Derrick Hammer | 9d259925f8 | |
Derrick Hammer | 277c3ff23f | |
Derrick Hammer | 161d71bfda | |
Derrick Hammer | bb41d70c10 | |
Derrick Hammer | 82532b01eb | |
Derrick Hammer | 9254dc20c8 | |
Derrick Hammer | fffe769ccf | |
Derrick Hammer | 775d3f7e6f | |
Derrick Hammer | 0679a7cc3b | |
Derrick Hammer | 0f1360c6df | |
Derrick Hammer | cb6ba5a24b | |
Derrick Hammer | d810cf0848 | |
Derrick Hammer | a16390ccc1 | |
Derrick Hammer | a54238d4b6 | |
Derrick Hammer | b267ace017 | |
Derrick Hammer | 90c8ce39eb | |
Derrick Hammer | 76b8def51d | |
Derrick Hammer | 87477ad559 | |
Derrick Hammer | 804f32e2be | |
Derrick Hammer | af5c7dc40d | |
Derrick Hammer | 12caef5f9b | |
Derrick Hammer | d078999108 | |
Derrick Hammer | 3331f1b1c1 | |
Derrick Hammer | 6a9eede07d | |
Derrick Hammer | 989ed70265 | |
Derrick Hammer | d03a781e18 | |
Derrick Hammer | 48da5d3fd4 | |
Derrick Hammer | 7ba826490e | |
Derrick Hammer | 0a5e52ec16 | |
Derrick Hammer | adc5bc213e | |
Derrick Hammer | 501de26793 | |
Derrick Hammer | 5a9f4a5940 | |
Derrick Hammer | ea5a97c613 | |
Derrick Hammer | 64eea68a84 | |
Derrick Hammer | 12a9ad28fd | |
Derrick Hammer | 4c2baf164f | |
Derrick Hammer | d40a954b67 | |
Derrick Hammer | c2d4ea7847 | |
Derrick Hammer | 23f462773f | |
Derrick Hammer | cf83dc6767 | |
Derrick Hammer | 90cdcd16af | |
Derrick Hammer | ccf1707f11 | |
Derrick Hammer | 98c576e2e8 | |
Derrick Hammer | 6d12ef9b94 | |
Derrick Hammer | ad23104700 | |
Derrick Hammer | 1f183c5052 | |
Derrick Hammer | 8b1c2f1065 | |
Derrick Hammer | c73b5c4d47 | |
Derrick Hammer | 46f81443c6 | |
Derrick Hammer | 1185a2a56e | |
Derrick Hammer | 73247a86fd | |
Derrick Hammer | c9531bc588 | |
Derrick Hammer | fc6240c149 | |
Derrick Hammer | eef49c34be | |
Derrick Hammer | f6be51b942 | |
Derrick Hammer | 334fa4d788 | |
Derrick Hammer | f2c68857f2 | |
Derrick Hammer | 0caa54f028 | |
Derrick Hammer | 96713e3538 | |
Derrick Hammer | ba60a6c729 | |
Derrick Hammer | 35b64515d7 | |
Derrick Hammer | 8d20659a5b | |
Derrick Hammer | e3022b8587 | |
Derrick Hammer | 66ddf67337 | |
Derrick Hammer | 085c4d69d6 | |
Derrick Hammer | 5b2a86275f | |
Derrick Hammer | 7665937196 | |
Derrick Hammer | 0a85711ead | |
Derrick Hammer | 7696997e53 | |
Derrick Hammer | 5523d5e60d | |
Derrick Hammer | 0e3a25aa8a | |
Derrick Hammer | f22506b413 | |
Derrick Hammer | d06f436fa1 | |
Derrick Hammer | 74cc88540d | |
Derrick Hammer | 755aff15da | |
Derrick Hammer | 62867f26a9 | |
Derrick Hammer | a48b10e50c | |
Derrick Hammer | a8f62fd666 | |
Derrick Hammer | 8bf4887dae | |
Derrick Hammer | 3e3f539a8b | |
Derrick Hammer | a289828c6f | |
Derrick Hammer | d895d047b3 | |
Derrick Hammer | abec1877cd | |
Derrick Hammer | 4dec430c69 | |
Derrick Hammer | facea33e0e | |
Derrick Hammer | be7a7977ac | |
Derrick Hammer | ee8fa2b98d | |
Derrick Hammer | ad8de8f5a1 | |
Derrick Hammer | 9587ef4941 | |
Derrick Hammer | 4a3028f61a | |
Derrick Hammer | da19a2e287 | |
Derrick Hammer | 52a1f18c60 | |
Derrick Hammer | 7df6bb245b | |
Derrick Hammer | cfce7348d4 | |
Derrick Hammer | 053a55c1f3 | |
Derrick Hammer | 6c0ae8c0e6 | |
Derrick Hammer | 9e170bae0d | |
Derrick Hammer | 7616d9f7c9 | |
Derrick Hammer | 2528fd0afe | |
Derrick Hammer | bee80a9981 | |
Derrick Hammer | b1fcc7f7ae | |
Derrick Hammer | b6c92a6348 | |
Derrick Hammer | f9c834752f | |
Derrick Hammer | 30aac94468 | |
Derrick Hammer | 79425b76fc | |
Derrick Hammer | 85738c1065 | |
Derrick Hammer | 7ed63d94cc | |
Derrick Hammer | c667c9509c | |
Derrick Hammer | 4988368b7c | |
Derrick Hammer | b55c1f7d48 | |
Derrick Hammer | ce93591ff8 | |
Derrick Hammer | 6ac37cfe65 | |
Derrick Hammer | ac61279081 | |
Derrick Hammer | d1bbe7c158 | |
Derrick Hammer | aff371a844 | |
Derrick Hammer | 3473551f6c | |
Derrick Hammer | 080bef354d | |
Derrick Hammer | 1d60cbf532 | |
Derrick Hammer | 3b3faaa1e6 | |
Derrick Hammer | 9ea77fb5c3 | |
Derrick Hammer | 9e52d35d2f | |
Derrick Hammer | 0bbb89e02c | |
Derrick Hammer | ebb19df217 | |
Derrick Hammer | bf6264b01d | |
Derrick Hammer | 080a4a1a85 | |
Derrick Hammer | 9bfdef1519 | |
Derrick Hammer | fddc64799e | |
Derrick Hammer | 4391e9fc31 | |
Derrick Hammer | b2b6102216 | |
Derrick Hammer | 2067c68a72 | |
Derrick Hammer | d1c5bde5c1 | |
Derrick Hammer | 26a6bda053 | |
Derrick Hammer | 93105fe5af | |
Derrick Hammer | 040c662826 | |
Derrick Hammer | 66f73d1a53 | |
Derrick Hammer | 48dc1b9be0 | |
Derrick Hammer | 9e5d996f20 | |
Derrick Hammer | 649e0e0011 | |
Derrick Hammer | 044604d863 | |
Derrick Hammer | fd53b98633 | |
Derrick Hammer | 5b8a7f79f0 | |
Derrick Hammer | 228cabd83b | |
Derrick Hammer | 99d47a4d9c | |
Derrick Hammer | 6b51e7196c | |
Derrick Hammer | dcab0b46cd | |
Derrick Hammer | 65278cb046 | |
Derrick Hammer | 455b793db6 | |
Derrick Hammer | 3b01c8642d | |
Derrick Hammer | c68dc51732 | |
Derrick Hammer | e864bcb098 | |
Derrick Hammer | e73ab26ebf | |
Derrick Hammer | 0e18f695cf | |
Derrick Hammer | fd75ec3f6a | |
Derrick Hammer | 9306051812 | |
Derrick Hammer | d893216831 | |
Derrick Hammer | 675a583422 | |
Derrick Hammer | 5861e95fb5 | |
Derrick Hammer | e7393085b4 | |
Derrick Hammer | 4bd2b028b7 | |
Derrick Hammer | 107118febc | |
Derrick Hammer | 244aa89d71 | |
Derrick Hammer | 9a899317c1 | |
Derrick Hammer | b4b211d003 | |
Derrick Hammer | 2a8c036dc6 | |
Derrick Hammer | 325a368dea | |
Derrick Hammer | 5223a44790 | |
Derrick Hammer | 33e644f5c7 | |
Derrick Hammer | 3dfdd2d2f4 | |
Derrick Hammer | 3e48593675 | |
Derrick Hammer | ae37a186a9 | |
Derrick Hammer | a85ced7c62 | |
Derrick Hammer | 61012ae394 | |
Derrick Hammer | 1bd4527300 | |
Derrick Hammer | 6c58f6bd6c | |
Derrick Hammer | 51c7211c39 | |
Derrick Hammer | b03e6815e2 | |
Derrick Hammer | 193871f083 | |
Derrick Hammer | 3dc5c72840 | |
Derrick Hammer | 1ca8d78c8e | |
Derrick Hammer | 57a455a17e | |
Derrick Hammer | 5431cac73d | |
Derrick Hammer | 41edceb11c | |
Derrick Hammer | fd6c3e8604 | |
Derrick Hammer | cc1efd5d85 | |
Derrick Hammer | f6b28b0ee0 | |
Derrick Hammer | 971c72ada9 | |
Derrick Hammer | f558d87b36 | |
Derrick Hammer | ccae147398 | |
Derrick Hammer | 8a2f501e8e | |
Derrick Hammer | 86c53d3c54 | |
Derrick Hammer | cafe863350 | |
Derrick Hammer | 41f9947429 | |
Derrick Hammer | 8b687d506f | |
Derrick Hammer | 52f462e03e | |
Derrick Hammer | 2d571e3484 | |
Derrick Hammer | ba0d32bb63 | |
Derrick Hammer | 7c6fec61b6 | |
Derrick Hammer | fae98f3d52 | |
Derrick Hammer | e11340ad2b | |
Derrick Hammer | e380dacced | |
Derrick Hammer | 37708c91f2 | |
Derrick Hammer | 7d87ed6ad7 | |
Derrick Hammer | 19afa09c4d | |
Derrick Hammer | cc63ff2c6e | |
Derrick Hammer | 358d5fdf60 | |
Derrick Hammer | d946e969bc | |
Derrick Hammer | 6ff84bbc1a | |
Derrick Hammer | d5118beb58 | |
Derrick Hammer | 749a932663 | |
Derrick Hammer | 4f891e067c | |
Derrick Hammer | 2e3ec1408e | |
Derrick Hammer | 2f0a538033 | |
Derrick Hammer | 0efcd35d65 | |
Derrick Hammer | bf8d909a3c | |
Derrick Hammer | ca12b99438 | |
Derrick Hammer | 36c614c483 | |
Derrick Hammer | c416b40d00 | |
Derrick Hammer | 06f37bf3d8 | |
Derrick Hammer | a1014acf15 | |
Derrick Hammer | d5782c7e86 | |
Derrick Hammer | 296f7b611e | |
Derrick Hammer | 9529c71b3c | |
Derrick Hammer | d5bed19c5d | |
Derrick Hammer | f33b73b533 | |
Derrick Hammer | edfce8b181 | |
Derrick Hammer | 87fa117df3 | |
Derrick Hammer | d99b994175 | |
Derrick Hammer | 4941949f22 | |
Derrick Hammer | c63f7ef50b | |
Derrick Hammer | 96ec3b8501 | |
Derrick Hammer | 944cb868b5 | |
Derrick Hammer | 0d9d7d4d98 | |
Derrick Hammer | 973c40afb4 | |
Derrick Hammer | a4bb3eadaa | |
Derrick Hammer | e7caa50932 | |
Derrick Hammer | 7f9887bdcc | |
Derrick Hammer | 6edf872664 | |
Derrick Hammer | 20a273a0dd | |
Derrick Hammer | d628ede1ee | |
Derrick Hammer | 9d17ae1065 | |
Derrick Hammer | 01cd30b6f8 | |
Derrick Hammer | 22a5c661bc | |
Derrick Hammer | 51d74b6483 | |
Derrick Hammer | 57c5088a1f | |
Derrick Hammer | 6eeed24707 | |
Derrick Hammer | af29081a3a | |
Derrick Hammer | 1568aa9007 | |
Derrick Hammer | 5687e72a32 | |
Derrick Hammer | e425c038e1 | |
Derrick Hammer | effb341418 | |
Derrick Hammer | 8c05180703 | |
Derrick Hammer | 21ae6d093d | |
Derrick Hammer | c9683453c0 | |
Derrick Hammer | ae178f003f | |
Derrick Hammer | b6683a5744 | |
Derrick Hammer | 39a39b00b7 | |
Derrick Hammer | dd296bd78a | |
Derrick Hammer | 736dc8aa9d | |
Derrick Hammer | 6c60dae743 | |
Derrick Hammer | f56377df2b | |
Derrick Hammer | 8c0a2451e4 | |
Derrick Hammer | 89bbd8b061 | |
Derrick Hammer | ce977da5c1 | |
Derrick Hammer | 770e3335d1 | |
Derrick Hammer | ab5356f024 | |
Derrick Hammer | 673cd535a0 | |
Derrick Hammer | 1f1f204b35 | |
Derrick Hammer | e89a9450e9 | |
Derrick Hammer | 0c90924f31 | |
Derrick Hammer | f3040399e4 | |
Derrick Hammer | 887f51f88d | |
Derrick Hammer | 4020b9f7c7 | |
Derrick Hammer | d0d67d2ae5 | |
Derrick Hammer | 1a1d34ef7a | |
Derrick Hammer | 1da600f1ca | |
Derrick Hammer | 2bff5d2b9e | |
Derrick Hammer | 5e31dc2f02 | |
Derrick Hammer | 4261bb6a5d | |
Derrick Hammer | e489de1e86 | |
Derrick Hammer | 8a6516e157 | |
Derrick Hammer | 6e97b582ba | |
Derrick Hammer | 3e4eed12ae | |
Derrick Hammer | 91d58ee87f | |
Derrick Hammer | 550398c701 | |
Derrick Hammer | 6fb77d102a | |
Derrick Hammer | 111d0a7ead | |
Derrick Hammer | dcbc54cec5 | |
Derrick Hammer | 1f008c40b4 | |
Derrick Hammer | 0896a70cfb | |
Derrick Hammer | 6479104d5a | |
Derrick Hammer | c4a124b521 | |
Derrick Hammer | caac09cc6f | |
Derrick Hammer | e6d51cd9e7 | |
Derrick Hammer | 0df51358d6 | |
Derrick Hammer | 14cd9fda7f | |
Derrick Hammer | cfc8ea4eac | |
Derrick Hammer | fdfffb897c | |
Derrick Hammer | a5c1356847 | |
Derrick Hammer | c0fa8d4ea3 | |
Derrick Hammer | 70e399cdd5 | |
Derrick Hammer | 56e076512b | |
Derrick Hammer | a169e4c3be | |
Derrick Hammer | 7791399fe4 | |
Derrick Hammer | a340967dd1 | |
Derrick Hammer | c6feee1351 | |
Derrick Hammer | ac3f0f8bdb | |
Derrick Hammer | 7d16ec5b7d | |
Derrick Hammer | 73623b8c36 | |
Derrick Hammer | f20b4ee916 | |
Derrick Hammer | f4799e2b7d | |
Derrick Hammer | 080fcbd559 | |
Derrick Hammer | bd5544198e | |
Derrick Hammer | 756a01d52f | |
Derrick Hammer | bd29ab4612 | |
Derrick Hammer | 9d378f4197 | |
Derrick Hammer | 201c9b992f | |
Derrick Hammer | 4483ad7ec5 | |
Derrick Hammer | 9d25784a6e | |
Derrick Hammer | d2c9f8e38a | |
Derrick Hammer | 53f9a8fb32 | |
Derrick Hammer | 82ae0d3ef7 | |
Derrick Hammer | ecf2532974 | |
Derrick Hammer | b03af65418 | |
Derrick Hammer | c2483aa7a9 | |
Derrick Hammer | c5b03ffd9e | |
Derrick Hammer | 4c91b0c2e7 | |
Derrick Hammer | 1688cf39f8 | |
Derrick Hammer | e71a160f46 | |
Derrick Hammer | 3d55254916 | |
Derrick Hammer | 9f65b1b455 | |
Derrick Hammer | c8124c0739 | |
Derrick Hammer | b2a2e13496 | |
Derrick Hammer | 37514a742f | |
Derrick Hammer | 4db07d5170 | |
Derrick Hammer | 2e9694149b | |
Derrick Hammer | f98b0f48bd | |
Derrick Hammer | dde5d255a6 | |
Derrick Hammer | 845de39049 | |
Derrick Hammer | b5c3d99568 | |
Derrick Hammer | ca06919764 | |
Derrick Hammer | 3654607f3f | |
Derrick Hammer | a96fc8682f | |
Derrick Hammer | 7315f8e694 | |
Derrick Hammer | 0c0cdfd2b1 | |
Derrick Hammer | a9c2ecade0 | |
Derrick Hammer | 8a112a8c12 | |
Derrick Hammer | 1dff84accf | |
Derrick Hammer | b87ba1e6bf | |
Derrick Hammer | 8e2adba1eb | |
Derrick Hammer | 3dbd791314 | |
Derrick Hammer | 022d6a94cc | |
Derrick Hammer | f79a0dd448 | |
Derrick Hammer | a1f36df8b9 | |
Derrick Hammer | 65548e8ec7 | |
Derrick Hammer | f63d567b53 | |
Derrick Hammer | 217bb78b3b | |
Derrick Hammer | ddc21014a8 | |
Derrick Hammer | a28bd6b11e | |
Derrick Hammer | 454deeae21 | |
Derrick Hammer | aefe9efaaa | |
Derrick Hammer | aacdd48428 | |
Derrick Hammer | b272e32185 | |
Derrick Hammer | d5e2770135 | |
Derrick Hammer | b46e12b972 | |
Derrick Hammer | 56100e5d50 | |
Derrick Hammer | 866739007c | |
Derrick Hammer | 8f6e7d1acc | |
Derrick Hammer | 7ec4f26142 | |
Derrick Hammer | a1572b256b | |
Derrick Hammer | 6427d24b0f | |
Derrick Hammer | 76cb45a3f4 | |
Derrick Hammer | 61832022f9 | |
Derrick Hammer | 823a1c68fc | |
Derrick Hammer | 32fed7e495 | |
Derrick Hammer | c19290c0fc | |
Derrick Hammer | 016ac72b8d | |
Derrick Hammer | a8c40fb998 | |
Derrick Hammer | 59247c5128 | |
Derrick Hammer | 038a1f0a88 | |
Derrick Hammer | fe793b33be | |
Derrick Hammer | 69de0440c1 | |
Derrick Hammer | cc41f87b14 | |
Derrick Hammer | eb26063015 | |
Derrick Hammer | 984e33d4a7 | |
Derrick Hammer | 652a80db3a | |
Derrick Hammer | 39beb89c77 | |
Derrick Hammer | 2245a9b24e | |
Derrick Hammer | bde35d28bd | |
Derrick Hammer | d2832cc67a | |
Derrick Hammer | 3f2757fb18 | |
Derrick Hammer | b3df326980 | |
Derrick Hammer | 4126c06cd8 | |
Derrick Hammer | 99a6f3a2f6 | |
Derrick Hammer | e32a9b2070 | |
Derrick Hammer | 64f52a87bd | |
Derrick Hammer | 754ab390f0 | |
Derrick Hammer | bb167ea360 | |
Derrick Hammer | 4fe856fd03 | |
Derrick Hammer | 7abba4ac8e | |
Derrick Hammer | 68d82390ed | |
Derrick Hammer | dd9fdabf47 | |
Derrick Hammer | 0b000bfc89 | |
Derrick Hammer | c984d72cfd | |
Derrick Hammer | 0d0ec43125 | |
Derrick Hammer | a49da3fdfe | |
Derrick Hammer | 5f0e2d2e15 | |
Derrick Hammer | b030de9714 | |
Derrick Hammer | b2325eb9af | |
Derrick Hammer | 55203fa466 | |
Derrick Hammer | fba98da0e0 | |
Derrick Hammer | 844d3a0a5f | |
Derrick Hammer | e45ab26a09 | |
Derrick Hammer | 72fed662e8 | |
Derrick Hammer | b756ad73e9 | |
Derrick Hammer | 6b7724eb51 | |
Derrick Hammer | 0ae69e5ba1 | |
Derrick Hammer | 80d34000a0 | |
Derrick Hammer | bfb8559e32 | |
Derrick Hammer | 586a6dc205 | |
Derrick Hammer | fcf43e1f79 | |
Derrick Hammer | d71849493b | |
Derrick Hammer | 9b82da72ca | |
Derrick Hammer | 960c2b01d9 | |
Derrick Hammer | cb558cdfc3 | |
Derrick Hammer | 1a20a7d35f | |
Derrick Hammer | 7edab13afe | |
Derrick Hammer | d9d85f2804 | |
Derrick Hammer | 5878d1557f | |
Derrick Hammer | 097e29aa94 | |
Derrick Hammer | cdb23540ca | |
Derrick Hammer | 9782907c90 | |
Derrick Hammer | 1ca46dddd4 | |
Derrick Hammer | b5509f11d1 | |
Derrick Hammer | 944c5f01b9 | |
Derrick Hammer | f9e8d4e2fc | |
Derrick Hammer | d002c56ffe | |
Derrick Hammer | c8a6570e8f | |
Derrick Hammer | 9f5b676f47 | |
Derrick Hammer | 7411228106 | |
Derrick Hammer | 01eda4aa23 | |
Derrick Hammer | 9b6a253313 | |
Derrick Hammer | 218c0a1b1a | |
Derrick Hammer | 355e8a3d22 | |
Derrick Hammer | b51f28ea64 | |
Derrick Hammer | 6e3e096be9 | |
Derrick Hammer | 8f8944a645 | |
Derrick Hammer | 89c03f9cee | |
Derrick Hammer | 86dc8c8b9a | |
Derrick Hammer | 43eb8c3e9a | |
Derrick Hammer | 6061cffcfd | |
Derrick Hammer | 5dfedeb66a | |
Derrick Hammer | d5c0157fd8 | |
Derrick Hammer | 5ed22efa83 | |
Derrick Hammer | 795bc5f82a | |
Derrick Hammer | fb03cd28b9 | |
Derrick Hammer | f89be1fef8 | |
Derrick Hammer | 33af108d39 | |
Derrick Hammer | 25b4286011 | |
Derrick Hammer | 057db6a636 | |
Derrick Hammer | e629618f25 | |
Derrick Hammer | 92c33e0af5 | |
Derrick Hammer | 036520581f | |
Derrick Hammer | 5c6224222f | |
Derrick Hammer | b3d63007e1 | |
Derrick Hammer | 355033634d | |
Derrick Hammer | 2573936000 | |
Derrick Hammer | b270d6f414 | |
Derrick Hammer | 88ef43acaa | |
Derrick Hammer | 90834601d7 | |
Derrick Hammer | 8965395fdf | |
Derrick Hammer | 25ebb00765 | |
Derrick Hammer | 1be4fb47fc | |
Derrick Hammer | 3da1ae3e5f | |
Derrick Hammer | 81e540c2ce | |
Derrick Hammer | 39f8152e09 | |
Derrick Hammer | 907de0b3af | |
Derrick Hammer | 3a0c7bdea2 | |
Derrick Hammer | 38375d44d8 | |
Derrick Hammer | 40b3504c1d | |
Derrick Hammer | 8cae7bba57 | |
Derrick Hammer | 5b210de198 | |
Derrick Hammer | 8885fd37cc | |
Derrick Hammer | d618f08275 | |
Derrick Hammer | 5c3d1144d4 | |
Derrick Hammer | 0010d6c5b9 | |
Derrick Hammer | de3b226df5 | |
Derrick Hammer | ea8c50edc7 | |
Derrick Hammer | fc40563ae4 | |
Derrick Hammer | f47552bf60 | |
Derrick Hammer | 988a313f93 | |
Derrick Hammer | 3c1e60c3d2 | |
Derrick Hammer | 5a78750df1 | |
Derrick Hammer | fb8dfb2fa2 | |
Derrick Hammer | f6f9a7f97a | |
Derrick Hammer | b5b0ed64b6 | |
Derrick Hammer | 995b227d7e | |
Derrick Hammer | 7987e597b0 | |
Derrick Hammer | 8602d5ed97 | |
Derrick Hammer | ee2f7331eb | |
Derrick Hammer | 1a9fa9c4be | |
Derrick Hammer | daf63268eb | |
Derrick Hammer | 6ddd10a4c9 | |
Derrick Hammer | 9932e6194e | |
Derrick Hammer | 7f5741a64b | |
Derrick Hammer | 856b7fb627 | |
Derrick Hammer | ef25887a87 | |
Derrick Hammer | 10060f0245 | |
Derrick Hammer | 051cf59195 | |
Derrick Hammer | bbfea6fc9e | |
Derrick Hammer | a6218e9b7c | |
Derrick Hammer | 939e329591 | |
Derrick Hammer | 41a3b1faa6 | |
Derrick Hammer | 4f094eab2c | |
Derrick Hammer | 0125fb4d01 | |
Derrick Hammer | a42cda1ced | |
Derrick Hammer | 7f12ee5b0d | |
Derrick Hammer | 99b97d9495 | |
Derrick Hammer | 901c68fdfc | |
Derrick Hammer | ca289818a9 | |
Derrick Hammer | a77981f0a6 | |
Derrick Hammer | ae7d048e6c | |
Derrick Hammer | b4f8a1979b | |
Derrick Hammer | 418cdf7836 | |
Derrick Hammer | e435e6ded5 | |
Derrick Hammer | 0e61a5c1fa | |
Derrick Hammer | 938b6cdf9e | |
Derrick Hammer | bff0981731 | |
Derrick Hammer | 0873dee1f3 | |
Derrick Hammer | 88a636ba9c | |
Derrick Hammer | 15750acec0 | |
Derrick Hammer | 8a32e69b06 | |
Derrick Hammer | 517abe9193 | |
Derrick Hammer | 16ed748bfb | |
Derrick Hammer | 2ce26239da | |
Derrick Hammer | 445084ca5b | |
Derrick Hammer | 9e35c614e3 | |
Derrick Hammer | 8f78f6fe87 | |
Derrick Hammer | fc042570ab | |
Derrick Hammer | a20b79ff90 | |
Derrick Hammer | cfb0abf81a | |
Derrick Hammer | 84a78b7a7e | |
Derrick Hammer | 0cb1f96813 | |
Derrick Hammer | 484b9ac583 | |
Derrick Hammer | 7ec03524ed | |
Derrick Hammer | fc1dd491d6 | |
Derrick Hammer | 2fd3368b5a | |
Derrick Hammer | ca559eccc5 | |
Derrick Hammer | bbf8ec79d3 | |
Derrick Hammer | 11f30700c3 | |
Derrick Hammer | c076d219d0 | |
Derrick Hammer | a546089378 | |
Derrick Hammer | 162af1e274 | |
Derrick Hammer | f73ad52864 | |
Derrick Hammer | 3ded11d705 | |
Derrick Hammer | 9949dae5e8 | |
Derrick Hammer | d4be04eae9 | |
Derrick Hammer | ac9a1a0b92 | |
Derrick Hammer | 263473db41 | |
Derrick Hammer | 997e362d90 | |
Derrick Hammer | 0ac4d318b7 | |
Derrick Hammer | 97297036c7 | |
Derrick Hammer | b52383b123 | |
Derrick Hammer | 6f3f6015fe | |
Derrick Hammer | 407114f527 | |
Derrick Hammer | 64492713f7 | |
Derrick Hammer | cdd458129b | |
Derrick Hammer | 76025f0a8a | |
Derrick Hammer | 857ffe4fdd | |
Derrick Hammer | afc0b7a343 | |
Derrick Hammer | 12a5f3f631 | |
Derrick Hammer | 30b18a4ced | |
Derrick Hammer | af0f3e19de | |
Derrick Hammer | 24694ecbae | |
Derrick Hammer | 3b9f4bbe3d | |
Derrick Hammer | 7c330e308e | |
Derrick Hammer | f34c041401 | |
Derrick Hammer | a5f0a4bfcc | |
Derrick Hammer | c452b0c271 | |
Derrick Hammer | b231f9d769 | |
Derrick Hammer | 6845dac609 | |
Derrick Hammer | c468a81543 | |
Derrick Hammer | 93e727ab3b | |
Derrick Hammer | c534162d6c | |
Derrick Hammer | d7da471b8b | |
Derrick Hammer | 1812b9cd38 | |
Derrick Hammer | a76d13e75d | |
Derrick Hammer | 44c564761c | |
Derrick Hammer | ffbb7e371a | |
Derrick Hammer | cd9cccc2a9 | |
Derrick Hammer | 7834471b84 | |
Derrick Hammer | 7f5847f7da | |
Derrick Hammer | f0d7a337db | |
Derrick Hammer | 1b3934c793 | |
Derrick Hammer | 16e8c84daa | |
Derrick Hammer | 24d491ec4e | |
Derrick Hammer | 1c3bfdc493 | |
Derrick Hammer | 3f90cbfe09 | |
Derrick Hammer | fc53bd3083 | |
Derrick Hammer | c084743b47 | |
Derrick Hammer | 829852c6c1 | |
Derrick Hammer | 593d8ea381 | |
Derrick Hammer | 41a6772c9f | |
Derrick Hammer | 143a563a51 | |
Derrick Hammer | 8b9471aa04 | |
Derrick Hammer | 634a285ea8 | |
Derrick Hammer | 96758a5559 | |
Derrick Hammer | 431dec55f9 | |
Derrick Hammer | 16689f6c31 | |
Derrick Hammer | 9d25690d72 | |
Derrick Hammer | 93e7563bed | |
Derrick Hammer | f645499c7f | |
Derrick Hammer | 50c4d8b945 | |
Derrick Hammer | 0b3d54e7c5 | |
Derrick Hammer | 9f6f2c9c87 | |
Derrick Hammer | 764a7cbdaf | |
Derrick Hammer | 171b810504 | |
Derrick Hammer | 8423578bdd | |
Derrick Hammer | f5bb0fa45f | |
Derrick Hammer | 2d3b755cb2 | |
Derrick Hammer | 9b748e1f57 | |
Derrick Hammer | 40b830d669 | |
Derrick Hammer | 23113d0f9c | |
Derrick Hammer | 6f61f09ba4 | |
Derrick Hammer | 302821d749 | |
Derrick Hammer | 3e629cf46e | |
Derrick Hammer | bbb68aecb5 | |
Derrick Hammer | 99c440ab88 | |
Derrick Hammer | 5598660176 | |
Derrick Hammer | 3c55ed2853 | |
Derrick Hammer | 75d9c7f46e | |
Derrick Hammer | 78accd1f02 | |
Derrick Hammer | 04948bde2c | |
Derrick Hammer | 94fd1a6af0 | |
Derrick Hammer | a43957b1db | |
Derrick Hammer | 227ac9b403 | |
Derrick Hammer | a4afda0ecc | |
Derrick Hammer | 0c00e2e7d9 | |
Derrick Hammer | 850b575e1c | |
Derrick Hammer | 28d966cbe2 | |
Derrick Hammer | e7ac46de32 | |
Derrick Hammer | fc9724df2c | |
Derrick Hammer | c790c525ae | |
Derrick Hammer | 2a1abb852b | |
Derrick Hammer | fc61da0d01 | |
Derrick Hammer | b939ea109c | |
Derrick Hammer | 7c945f0a2d | |
Derrick Hammer | 89cdd01698 | |
Derrick Hammer | 8e04de591e | |
Derrick Hammer | 172b040365 | |
Derrick Hammer | 279cc484fc | |
Derrick Hammer | 69ae351d94 | |
Derrick Hammer | 192ac364c5 | |
Derrick Hammer | 0f3f92442a | |
Derrick Hammer | be92e036f3 | |
Derrick Hammer | d13c15212c | |
Derrick Hammer | 6655abe61b | |
Derrick Hammer | 8a1e586b28 | |
Derrick Hammer | 39b4977d52 | |
Derrick Hammer | 130abe6098 | |
Derrick Hammer | 5784afe064 | |
Derrick Hammer | 6894cd7e54 | |
Derrick Hammer | 6323fbe166 | |
Derrick Hammer | d57a14d9a3 | |
Derrick Hammer | fd721077e5 | |
Derrick Hammer | b82353cfa9 | |
Derrick Hammer | a3846a8e07 | |
Derrick Hammer | 185f5de87a | |
Derrick Hammer | 9063a80f8c | |
Derrick Hammer | 5456773b81 | |
Derrick Hammer | dc9b3b4fda | |
Derrick Hammer | c1915321e1 | |
Derrick Hammer | 9825c904da | |
Derrick Hammer | d15ec4e81e | |
Derrick Hammer | 9330bb36bf | |
Derrick Hammer | 0a6efaf0e3 | |
Derrick Hammer | 8abc41e46f | |
Derrick Hammer | 446e81ca19 | |
Derrick Hammer | 15b527933f | |
Derrick Hammer | 95cfa393b4 | |
Derrick Hammer | 9a87004f31 | |
Derrick Hammer | d88638dfc3 | |
Derrick Hammer | aff6e8106c | |
Derrick Hammer | 6d34f5b683 | |
Derrick Hammer | 8449b13a4a | |
Derrick Hammer | 2de10a7401 | |
Derrick Hammer | 57c4a1ae7d | |
Derrick Hammer | 7b24a2001c | |
Derrick Hammer | e787efaa92 | |
Derrick Hammer | 8dec7fc75a | |
Derrick Hammer | 56ddeef010 | |
Derrick Hammer | 791b291ce7 | |
Derrick Hammer | 435fe8b754 | |
Derrick Hammer | 883504225e | |
Derrick Hammer | 64c3795876 | |
Derrick Hammer | 3a49375638 | |
Derrick Hammer | e477d681d4 | |
Derrick Hammer | b86b597cb8 | |
Derrick Hammer | 9224dcd119 | |
Derrick Hammer | a4737ab4b8 | |
Derrick Hammer | 45567fcda0 | |
Derrick Hammer | 1721fbf832 | |
Derrick Hammer | 73a1e1cffe | |
Derrick Hammer | 2a745d6498 | |
Derrick Hammer | 741647c6dc | |
Derrick Hammer | 27240f9f30 | |
Derrick Hammer | aaa39d810e | |
Derrick Hammer | 81145ef404 | |
Derrick Hammer | 77ebdf9f4c | |
Derrick Hammer | 34ba5b42f8 | |
Derrick Hammer | e034e1d54e | |
Derrick Hammer | 9b891f6ec7 | |
Derrick Hammer | 6b66250c08 | |
Derrick Hammer | a2ee46dbb3 | |
Derrick Hammer | 4bb34315eb | |
Derrick Hammer | 2a067102da | |
Derrick Hammer | 1af1ea9505 | |
Derrick Hammer | b4e2e962e5 | |
Derrick Hammer | d51c52b985 | |
Derrick Hammer | 309cc50845 | |
Derrick Hammer | 46da3bdcee | |
Derrick Hammer | 47422524b8 | |
Derrick Hammer | e6c1bab602 | |
Derrick Hammer | 470bce2209 | |
Derrick Hammer | 18a54917a0 | |
Derrick Hammer | 568ec2857a | |
Derrick Hammer | 610d5fe268 | |
Derrick Hammer | 6c31a0a79f | |
Derrick Hammer | 646a65f814 | |
Derrick Hammer | a410cc55f0 | |
Derrick Hammer | d212907f5d | |
Derrick Hammer | 4348ff6dfe | |
Derrick Hammer | 57516a2f4a | |
Derrick Hammer | 2aec82281d | |
Derrick Hammer | c326d9b61b | |
Derrick Hammer | 5fd601407f | |
Derrick Hammer | ceabb95f6d | |
Derrick Hammer | 10ecaebbf2 | |
Derrick Hammer | 98c1784518 | |
Derrick Hammer | 2887a63a7a | |
Derrick Hammer | faa7387106 | |
Derrick Hammer | ab3dd648e1 | |
Derrick Hammer | 9bb7a4cc83 | |
Derrick Hammer | 4a66be5b87 | |
Derrick Hammer | 80484079d6 | |
Derrick Hammer | 8c89796341 | |
Derrick Hammer | f7057142eb | |
Derrick Hammer | 55f515157d | |
Derrick Hammer | 92cddb40c3 | |
Derrick Hammer | 2dc8fc56f5 | |
Derrick Hammer | ad54cc70b3 | |
Derrick Hammer | c051ef8e44 | |
Derrick Hammer | be27728b42 | |
Derrick Hammer | f3be950ba7 | |
Derrick Hammer | 00a58a3b98 | |
Derrick Hammer | 541fcff779 | |
Derrick Hammer | 73bd74faeb | |
Derrick Hammer | d21044baed | |
Derrick Hammer | e00922f49d | |
Derrick Hammer | 15ba6e9695 | |
Derrick Hammer | a90344daf0 | |
Derrick Hammer | f4b981f97f | |
Derrick Hammer | 46b407bd9a | |
Derrick Hammer | e1556f2f68 | |
Derrick Hammer | d91355796b | |
Derrick Hammer | 8797460bf8 | |
Derrick Hammer | 2c7300af6d | |
Derrick Hammer | fce08283a2 | |
Derrick Hammer | 6d8beb0331 | |
Derrick Hammer | 8f138a5df7 | |
Derrick Hammer | 263f34b89f | |
Derrick Hammer | 8528df5d96 | |
Derrick Hammer | f544c30430 | |
Derrick Hammer | ce95437191 | |
Derrick Hammer | 84bb08144b | |
Derrick Hammer | 14d8760c1f | |
Derrick Hammer | c3646fa4d4 | |
Derrick Hammer | 7b7c705c0d | |
Derrick Hammer | 6cac5c6a28 | |
Derrick Hammer | 6d998eeff4 | |
Derrick Hammer | bf15faf33f | |
Derrick Hammer | bf25d7bfda | |
Derrick Hammer | 2f9b684953 | |
Derrick Hammer | dcf05974e2 | |
Derrick Hammer | 95b57cffc0 | |
Derrick Hammer | 562742fd8e | |
Derrick Hammer | 47020fe738 | |
Derrick Hammer | 5db53f6a21 | |
Derrick Hammer | f51c06bc24 | |
Derrick Hammer | 4dd1f50dab | |
Derrick Hammer | 4bb89fedf1 | |
Derrick Hammer | 523286df32 | |
Derrick Hammer | 9b655b4a70 | |
Derrick Hammer | 60c905181c | |
Derrick Hammer | c976ec31be | |
Derrick Hammer | e476ed4476 | |
Derrick Hammer | cd74127a6b | |
Derrick Hammer | 4b9e362437 | |
Derrick Hammer | cc6df265d4 | |
Derrick Hammer | 7f2e38291e | |
Derrick Hammer | fe2d1be764 | |
Derrick Hammer | 77a4561f55 | |
Derrick Hammer | b3f8b483db | |
Derrick Hammer | 9002064937 | |
Derrick Hammer | 60a591f70b | |
Derrick Hammer | a93eca6a7c | |
Derrick Hammer | fdfdf9b6a9 | |
Derrick Hammer | a4137102e6 | |
Derrick Hammer | 6a2b1b4a9b | |
Derrick Hammer | 35fa1c5e0d | |
Derrick Hammer | 87fb81bf97 | |
Derrick Hammer | 27cbe2d886 | |
Derrick Hammer | 5d715fcac4 | |
Derrick Hammer | efcd5b0b8a | |
Derrick Hammer | 1948b9e332 | |
Derrick Hammer | 22eacc4af1 | |
Derrick Hammer | 12093637ed | |
Derrick Hammer | fb1112f3a2 | |
Derrick Hammer | 8df2ee9ee8 | |
Derrick Hammer | 7b96682ce0 | |
Derrick Hammer | 1643dacdd4 | |
Derrick Hammer | 0eb67cd8da | |
Derrick Hammer | 11e533577b | |
Derrick Hammer | da298cc56f | |
Derrick Hammer | bf36562fca | |
Derrick Hammer | 3fcb897e7a | |
Derrick Hammer | 941ce27293 | |
Derrick Hammer | e0c6c88e75 | |
Derrick Hammer | aab4bb4d69 | |
Derrick Hammer | 35cd041978 | |
Derrick Hammer | 2020a9f1d1 | |
Derrick Hammer | 6402410d75 | |
Derrick Hammer | 2cc600b78b | |
Derrick Hammer | 527334f829 | |
Derrick Hammer | dd857650e0 | |
Derrick Hammer | d86e0e0105 | |
Derrick Hammer | e0de290cff | |
Derrick Hammer | 3e0246df28 | |
Derrick Hammer | ed15133659 | |
Derrick Hammer | ae4901757b | |
Derrick Hammer | e73fa0a103 | |
Derrick Hammer | c2cccc4b84 | |
Derrick Hammer | 6bee380e75 | |
Derrick Hammer | bcf2998faa | |
Derrick Hammer | 7d80efb278 | |
Derrick Hammer | 3a74e75a57 | |
Derrick Hammer | 521b37b642 | |
Derrick Hammer | 08e034b1cf | |
Derrick Hammer | b10798d71f | |
Derrick Hammer | 43c4590439 | |
Derrick Hammer | ea4a22c52d | |
Derrick Hammer | 6bbfac661a | |
Derrick Hammer | 112fbb4c51 | |
Derrick Hammer | 5465cf7a63 | |
Derrick Hammer | 60e917120d | |
Derrick Hammer | c5b0865977 | |
Derrick Hammer | 9f7f819369 | |
Derrick Hammer | e201f97e0b | |
Derrick Hammer | 75e7ba00cd | |
Derrick Hammer | 07c36109d8 | |
Derrick Hammer | 24e841ae97 | |
Derrick Hammer | a1e7cda659 | |
Derrick Hammer | 9bf10b19bf | |
Derrick Hammer | 73fa265939 | |
Derrick Hammer | a56fa20b6d | |
Derrick Hammer | 1d1c552a0a | |
Derrick Hammer | a2051acff1 | |
Derrick Hammer | 4378da70da | |
Derrick Hammer | 8c86ecc5b7 | |
Derrick Hammer | e8fbe46dfc | |
Derrick Hammer | 0ab70dcaa5 | |
Derrick Hammer | af5b6241bf | |
Derrick Hammer | b50c6c9f85 | |
Derrick Hammer | 43e52e1ae1 | |
Derrick Hammer | fdef217078 | |
Derrick Hammer | fba3ee4213 | |
Derrick Hammer | eb063a8954 | |
Derrick Hammer | eaa515345e | |
Derrick Hammer | 48e3c690ce | |
Derrick Hammer | 16a3b531ac | |
Derrick Hammer | c397cc9fcb | |
Derrick Hammer | 60c7cc5c6c | |
Derrick Hammer | 1f7c05434a | |
Derrick Hammer | 72219eb59c | |
Derrick Hammer | 2e64b56115 | |
Derrick Hammer | 5b1838a63b | |
Derrick Hammer | 26c28db1f2 | |
Derrick Hammer | e9db71f3b8 | |
Derrick Hammer | 4c92750dd0 | |
Derrick Hammer | 2c30477465 | |
Derrick Hammer | 6acf8a606a | |
Derrick Hammer | b50c16ff2a | |
Derrick Hammer | 6a8936b9c9 | |
Derrick Hammer | 1f8ad3ac1a | |
Derrick Hammer | 0eb6a9a3a3 | |
Derrick Hammer | 5323e43bdb | |
Derrick Hammer | fbbb22145d | |
Derrick Hammer | 2693c892a4 | |
Derrick Hammer | 2500b3f047 | |
Derrick Hammer | 5b6084986f | |
Derrick Hammer | 8044591697 | |
Derrick Hammer | 82f34726d6 | |
Derrick Hammer | 12de0342f5 | |
Derrick Hammer | 8161d36f0e | |
Derrick Hammer | 17fdad7d07 | |
Derrick Hammer | 31b6a70180 | |
Derrick Hammer | 677635aa08 | |
Derrick Hammer | 1a62ab2855 | |
Derrick Hammer | ad0e6964eb | |
Derrick Hammer | 4e72ddbde8 | |
Derrick Hammer | f957ef5d78 | |
Derrick Hammer | 510a57162c | |
Derrick Hammer | 0c88e80a66 | |
Derrick Hammer | dfd03673c9 | |
Derrick Hammer | 6545faad6a | |
Derrick Hammer | 310c23b95e | |
Derrick Hammer | 7fde67aea5 | |
Derrick Hammer | b56a8ba5ac | |
Derrick Hammer | 8ff09b5f02 | |
Derrick Hammer | b3e1840fac | |
Derrick Hammer | 72c3167e5f | |
Derrick Hammer | acb9604b02 | |
Derrick Hammer | cde3f90d2d | |
Derrick Hammer | 1fcd7fdfdc | |
Derrick Hammer | 7248570e6b | |
Derrick Hammer | 32be5fe6e1 | |
Derrick Hammer | c338a41efd | |
Derrick Hammer | 5fec2f08ff | |
Derrick Hammer | 8c4687fd67 | |
Derrick Hammer | d16731807c | |
Derrick Hammer | 6a8a3c436a | |
Derrick Hammer | 66dabf5150 | |
Derrick Hammer | 1a5aaa3927 | |
Derrick Hammer | a5cbb4c4fb | |
Derrick Hammer | 1cf2d9880c | |
Derrick Hammer | cc61a090b6 | |
Derrick Hammer | cf422aef0e | |
Derrick Hammer | ef872bf344 | |
Derrick Hammer | 897fec75ad | |
Derrick Hammer | da86c96c87 | |
Derrick Hammer | 708bd82879 | |
Derrick Hammer | bccd919872 | |
Derrick Hammer | 78f789b2c0 | |
Derrick Hammer | f779a61a76 | |
Derrick Hammer | 03f0d80ae5 | |
Derrick Hammer | a41cdbf52c | |
Derrick Hammer | 1d019d905b | |
Derrick Hammer | af71f68ea9 | |
Derrick Hammer | bf65e845f3 | |
Derrick Hammer | e9aa676d94 | |
Derrick Hammer | 224d7a636d | |
Derrick Hammer | e31672aad0 | |
Derrick Hammer | 4ae272205a | |
Derrick Hammer | ae0bddf3d1 | |
Derrick Hammer | 1054c52e2f | |
Derrick Hammer | dd66f560ef | |
Derrick Hammer | d237b42314 | |
Derrick Hammer | f8d0dc6787 | |
Derrick Hammer | 9cc05b3096 | |
Derrick Hammer | 6c34b383d7 | |
Derrick Hammer | bc5957f881 | |
Derrick Hammer | accffade40 | |
Derrick Hammer | f34f009f17 | |
Derrick Hammer | e1709a7910 | |
Derrick Hammer | d74d29e2c5 | |
Derrick Hammer | 09216e2817 | |
Derrick Hammer | db46fcd774 | |
Derrick Hammer | 1b680dd399 | |
Derrick Hammer | dd5c6332f3 | |
Derrick Hammer | 5d393c3915 | |
Derrick Hammer | 3be1042def | |
Derrick Hammer | 56b99f3c14 | |
Derrick Hammer | 40479d8bb3 | |
Derrick Hammer | ec9026c8b1 | |
Derrick Hammer | eee1faab18 | |
Derrick Hammer | 17441ff674 | |
Derrick Hammer | 891ca20a72 | |
Derrick Hammer | 2512a6bdaf | |
Derrick Hammer | 9ad8d70f09 | |
Derrick Hammer | 1bbedecad9 | |
Derrick Hammer | c4f0226d1a | |
Derrick Hammer | 69b1938e87 | |
Derrick Hammer | a62c6daa4a | |
Derrick Hammer | fb136234a9 | |
Derrick Hammer | 4b581d5879 | |
Derrick Hammer | 185269afea | |
Derrick Hammer | cc5fadeef3 | |
Derrick Hammer | aa2ee9eee2 | |
Derrick Hammer | d017b0741c | |
Derrick Hammer | af4a2eed4a | |
Derrick Hammer | d36bf67e85 | |
Derrick Hammer | c2075989fa | |
Derrick Hammer | 0c5827ce0b | |
Derrick Hammer | a19af267cf | |
Derrick Hammer | 58f734d3b3 | |
Derrick Hammer | 276719f47f | |
Derrick Hammer | a4e0e1fa58 | |
Derrick Hammer | 866d105028 | |
Derrick Hammer | 48f03c0f47 | |
Derrick Hammer | 441e07f00e | |
Derrick Hammer | f986e3e483 | |
Derrick Hammer | 61696f42b8 | |
Derrick Hammer | 62e22d0d39 | |
Derrick Hammer | 47602854a0 | |
Derrick Hammer | 4a408a179d | |
Derrick Hammer | 4976874453 | |
Derrick Hammer | 86c80aefaa | |
Derrick Hammer | d30d2f34b5 | |
Derrick Hammer | 539d5ead65 | |
Derrick Hammer | cd7790834b | |
Derrick Hammer | 4b56de03a3 | |
Derrick Hammer | 1ccc5d1141 | |
Derrick Hammer | 46dbfe77bc | |
Derrick Hammer | e40111a276 | |
Derrick Hammer | b2d863d466 | |
Derrick Hammer | 17f16c53af | |
Derrick Hammer | f0b92aa2d6 | |
Derrick Hammer | 87ebf5012b | |
Derrick Hammer | 212832eda1 | |
Derrick Hammer | a8a39d523e | |
Derrick Hammer | 27ad581026 | |
Derrick Hammer | 6076073dce | |
Derrick Hammer | 51a1f6051b | |
Derrick Hammer | 05c0aba484 | |
Derrick Hammer | b9fab1a4b5 | |
Derrick Hammer | 3a44305c44 | |
Derrick Hammer | 82fe380b02 | |
Derrick Hammer | 2040d4edbc | |
Derrick Hammer | 66e1cba39b | |
Derrick Hammer | 8ceb8c1eb9 | |
Derrick Hammer | eae3de419c | |
Derrick Hammer | 6784cbf453 | |
Derrick Hammer | 387ec56bec | |
Derrick Hammer | 0cd60b7db0 | |
Derrick Hammer | 699869ca3e | |
Derrick Hammer | 138b7a8e8c | |
Derrick Hammer | d4ed4eb9a2 | |
Derrick Hammer | dc239b0cba | |
Derrick Hammer | 61304ad3d2 | |
Derrick Hammer | e4626fdbb8 | |
Derrick Hammer | d6d15cff73 | |
Derrick Hammer | b0a7566466 | |
Derrick Hammer | 01029430cb | |
Derrick Hammer | f080dbc943 | |
Derrick Hammer | 2ef6df540f | |
Derrick Hammer | a8f9dfdbd3 | |
Derrick Hammer | 0c49c70b98 | |
Derrick Hammer | a641e57656 | |
Derrick Hammer | df815c1ebf | |
Derrick Hammer | ef9ecbf8e9 | |
Derrick Hammer | 73ab5b8214 | |
Derrick Hammer | 8ab200541e | |
Derrick Hammer | 4b1622511e | |
Derrick Hammer | 41481fe89d | |
Derrick Hammer | eb9f4513b5 | |
Derrick Hammer | 3f07a580ec | |
Derrick Hammer | 0281936511 | |
Derrick Hammer | ba44b58897 | |
Derrick Hammer | 592b20c561 | |
Derrick Hammer | 4f50d645ad | |
Derrick Hammer | 35aa206687 | |
Derrick Hammer | a16bcc788d | |
Derrick Hammer | 7ef2f819a8 | |
Derrick Hammer | 908411f33f | |
Derrick Hammer | 9ebdeb74c6 | |
Derrick Hammer | 492bd5b9de | |
Derrick Hammer | 8afc157d4f | |
Derrick Hammer | 27773a7909 | |
Derrick Hammer | 27375a858f | |
Derrick Hammer | 7fd30e571c | |
Derrick Hammer | 89935f2f00 | |
Derrick Hammer | 59a3ac6b64 | |
Derrick Hammer | 866fa89cee | |
Derrick Hammer | ab6b71813d | |
Derrick Hammer | de407b2803 | |
Derrick Hammer | 4819bf45ac | |
Derrick Hammer | 1e3addac8b | |
Derrick Hammer | 2caaa07da8 | |
Derrick Hammer | fecc99b081 | |
Derrick Hammer | 4d75f5659c | |
Derrick Hammer | 862570bc57 | |
Derrick Hammer | e6f3df2882 | |
Derrick Hammer | 522ed11d50 | |
Derrick Hammer | 0bd089e046 | |
Derrick Hammer | b5c540ab8e | |
Derrick Hammer | 2fb08f35ed | |
Derrick Hammer | 7ccfaa57da | |
Derrick Hammer | 5fbac2ae13 | |
Derrick Hammer | fa4a3e6f2a | |
Derrick Hammer | c7af6286ad | |
Derrick Hammer | 8917be5077 | |
Derrick Hammer | b8572ea712 | |
Derrick Hammer | a54341dd68 | |
Derrick Hammer | a57e575a82 | |
Derrick Hammer | 519426ba7d | |
Derrick Hammer | 82aded34ac | |
Derrick Hammer | 959005f29c | |
Derrick Hammer | 444de35e31 | |
Derrick Hammer | c80046e95f | |
Derrick Hammer | 8e8ea8ce86 | |
Derrick Hammer | c7bce2ff23 | |
semantic-release-bot | 540457fb2f | |
Derrick Hammer | d1b0aa5139 | |
Derrick Hammer | a06b79a537 | |
semantic-release-bot | fdd6b08b71 | |
Derrick Hammer | ae071a30ec | |
Derrick Hammer | 37cdbfbc0d | |
Derrick Hammer | 2f64f18e24 | |
semantic-release-bot | 66b9cd2022 | |
Derrick Hammer | 9879662d5b | |
Derrick Hammer | cd2f63eb72 | |
Derrick Hammer | 3e80bb43fa | |
Derrick Hammer | e100429b60 | |
Derrick Hammer | ee17409e12 | |
Derrick Hammer | 18529f2cd1 | |
Derrick Hammer | 06b3ab87f7 | |
Derrick Hammer | 18e102cc8a | |
Derrick Hammer | f11b285d4e | |
Derrick Hammer | a7ac5a5b72 | |
Derrick Hammer | e2db880038 | |
Derrick Hammer | e09e51bb52 | |
Derrick Hammer | dff3ca4589 | |
Derrick Hammer | 8d3f490c01 | |
Derrick Hammer | 78ee15cf4b | |
Derrick Hammer | 1cfc2223a6 | |
Derrick Hammer | a23d165caa | |
Derrick Hammer | 934f8e6236 | |
Derrick Hammer | 504dcefb35 | |
Derrick Hammer | 76d3043dda | |
Derrick Hammer | faaec649ea | |
Derrick Hammer | ceb729f11d | |
Derrick Hammer | 0bc862e35d | |
Derrick Hammer | 53f29c99bc | |
Derrick Hammer | e018a4b743 | |
Derrick Hammer | 637b656d36 | |
Derrick Hammer | 5d15ca330a | |
Derrick Hammer | 993b9e8208 | |
Derrick Hammer | 66f2545781 | |
Derrick Hammer | 2062562f6b | |
Derrick Hammer | b122626e97 | |
Derrick Hammer | 976394b29d | |
Derrick Hammer | 914313a585 | |
Derrick Hammer | 1f5a3d19e4 |
|
@ -0,0 +1,35 @@
|
|||
name: Build/Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_enabled:
|
||||
description: Debug
|
||||
type: boolean
|
||||
default: false
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- develop-*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install SSH key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.GITEA_SSH_KEY }}
|
||||
known_hosts: ${{ secrets.GITEA_KNOWN_HOST }}
|
||||
- name: Publish
|
||||
uses: go-semantic-release/action@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prerelease: true
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled && failure() }}
|
||||
with:
|
||||
limit-access-to-actor: true
|
|
@ -0,0 +1,4 @@
|
|||
[submodule "api/account/app"]
|
||||
path = api/account/app
|
||||
url = https://git.lumeweb.com/LumeWeb/portal-dashboard.git
|
||||
branch = develop
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"plugins": {
|
||||
"commit-analyzer": {
|
||||
"name": "default@^1.0.0"
|
||||
},
|
||||
"ci-condition": {
|
||||
"name": "github",
|
||||
"options": {
|
||||
"defaultBranch": "*"
|
||||
}
|
||||
},
|
||||
"changelog-generator": {
|
||||
"name": "default",
|
||||
"options": {
|
||||
"emojis": "true"
|
||||
}
|
||||
},
|
||||
"provider": {
|
||||
"name": "git",
|
||||
"options": {
|
||||
"default_branch": "develop",
|
||||
"tagger_email": "gitea@git.lumeweb.com",
|
||||
"auth": "ssh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
# [0.1.0-develop.3](https://git.lumeweb.com/LumeWeb/portal/compare/v0.1.0-develop.2...v0.1.0-develop.3) (2023-09-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle failure on verifying token ([a06b79a](https://git.lumeweb.com/LumeWeb/portal/commit/a06b79a537f08d741faeb8319d558c9e64977c4b))
|
||||
|
||||
# [0.1.0-develop.2](https://git.lumeweb.com/LumeWeb/portal/compare/v0.1.0-develop.1...v0.1.0-develop.2) (2023-08-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* need to change dnslink route registration to use a path param based route ([ae071a3](https://git.lumeweb.com/LumeWeb/portal/commit/ae071a30ecaa62ff431878c71a54059e3d3ce8b7))
|
||||
* need to string off forward slash at beginning to match manifest file paths ([2f64f18](https://git.lumeweb.com/LumeWeb/portal/commit/2f64f18e24fa1e4ddd74ed6a8d2d44e483fff1dc))
|
||||
|
||||
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/portal/compare/v0.0.1...v0.1.0-develop.1) (2023-08-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* abort if we don't have a password for the account, assume its pubkey only ([c20dec0](https://git.lumeweb.com/LumeWeb/portal/commit/c20dec020437d91cf2728852b8bed5c4a0c481e9))
|
||||
* add a check for a 500 error ([df08fc9](https://git.lumeweb.com/LumeWeb/portal/commit/df08fc980ac3f710a67bd692b8126eb978699d5b))
|
||||
* add missing request connection close ([dff3ca4](https://git.lumeweb.com/LumeWeb/portal/commit/dff3ca45895095b82ba2e76b2e61487e28151b7d))
|
||||
* add shutdown signal and flag for renterd ([fb65690](https://git.lumeweb.com/LumeWeb/portal/commit/fb65690abd5c190dce30d3cfe0d079b27040a309))
|
||||
* **auth:** eager load the account relation to return it ([a23d165](https://git.lumeweb.com/LumeWeb/portal/commit/a23d165caa3ba4832c9d37a0b833b9b58df60732))
|
||||
* change jwtKey to ed25519.PrivateKey ([bf576df](https://git.lumeweb.com/LumeWeb/portal/commit/bf576dfaeef51078d7bdae885550fc235d49c1eb))
|
||||
* close db on shutdown ([78ee15c](https://git.lumeweb.com/LumeWeb/portal/commit/78ee15cf4b5d3a55209a9c7559700a2c5b227f87))
|
||||
* Ctx must be public ([a0d747f](https://git.lumeweb.com/LumeWeb/portal/commit/a0d747fdf4e6ee3fa6a3b4dca180e4f14af30ed9))
|
||||
* ctx needs to be public in AuthService ([a3cfeba](https://git.lumeweb.com/LumeWeb/portal/commit/a3cfebab307a87bc895d7b1c1f0e6632a708562c))
|
||||
* **db:** need to set charset, parseTime and loc in connection for mysql ([5d15ca3](https://git.lumeweb.com/LumeWeb/portal/commit/5d15ca330abd26576ef9865c110975aeb27c3ab3))
|
||||
* disable client warnings ([9b8cb38](https://git.lumeweb.com/LumeWeb/portal/commit/9b8cb38496541b0ab50d28eef63658f9723c5802))
|
||||
* dont try to stream if we have an error ([b21a425](https://git.lumeweb.com/LumeWeb/portal/commit/b21a425e24f5543802e7267369f37967d4805697))
|
||||
* encode size as uint64 to the end of the cid ([5aca66d](https://git.lumeweb.com/LumeWeb/portal/commit/5aca66d91981d8fae88194df6b03c239dbd179a8))
|
||||
* ensure all models auto increment the id field ([934f8e6](https://git.lumeweb.com/LumeWeb/portal/commit/934f8e6236ef1eef8db1d06a1d7a7fded8afe694))
|
||||
* ensure we store the pubkey in lowercase ([def1b50](https://git.lumeweb.com/LumeWeb/portal/commit/def1b50cfcba8c68f3b95209790418638374fad9))
|
||||
* handle duplicate tus uploads by hash ([f3172b0](https://git.lumeweb.com/LumeWeb/portal/commit/f3172b0d31f844b95a0e64b3a5d821f71b0fbe07))
|
||||
* hasher needs the size set to 32 ([294370d](https://git.lumeweb.com/LumeWeb/portal/commit/294370d88dd159ae173a6a955a417a1547de60ed))
|
||||
* if upload status code isn't 200, make it an err based on the body ([039a4a3](https://git.lumeweb.com/LumeWeb/portal/commit/039a4a33547a59b4f3ec86199664b5bb94d258a6))
|
||||
* if uploading returns a 500 and its a slab error, treat as a 404 ([6ddef03](https://git.lumeweb.com/LumeWeb/portal/commit/6ddef03790971e346fa0a7d33a462f39348bc6cc))
|
||||
* if we have an existing upload, just return it as if successful ([90170e5](https://git.lumeweb.com/LumeWeb/portal/commit/90170e5b81831f3d768291fd37c7c13e32d522fe))
|
||||
* iris context.User needs to be embedded in our User struct for type checking to properly work ([1cfc222](https://git.lumeweb.com/LumeWeb/portal/commit/1cfc2223a6df614f26fd0337ced68d92e774589f))
|
||||
* just use the any route ([e100429](https://git.lumeweb.com/LumeWeb/portal/commit/e100429b60e783f6c7c3ddecab7bb9b4dd599726))
|
||||
* load awsConfig before db ([58165e0](https://git.lumeweb.com/LumeWeb/portal/commit/58165e01af9f2b183d654d3d8809cbd1eda0a9bb))
|
||||
* make an attempt to look for the token before adding to db ([f11b285](https://git.lumeweb.com/LumeWeb/portal/commit/f11b285d4e255c1c4c95f6ac15aa904d7a5730e4))
|
||||
* missing setting SetTusComposer ([80561f8](https://git.lumeweb.com/LumeWeb/portal/commit/80561f89e92dfa86887ada8361e0046ee6288234))
|
||||
* newer gorm version causes db rebuilds every boot ([72255eb](https://git.lumeweb.com/LumeWeb/portal/commit/72255eb3c50892aa5f2cfdc4cb1daa5883f0affc))
|
||||
* only panic if the error is other than a missing awsConfig file ([6e0ec8a](https://git.lumeweb.com/LumeWeb/portal/commit/6e0ec8aaf90e86bcb7cb6c8c53f6569e6885e0aa))
|
||||
* output error info ([cfa7ceb](https://git.lumeweb.com/LumeWeb/portal/commit/cfa7ceb2f422a6e594a424315c8eaeffc6572926))
|
||||
* PostPubkeyChallenge should be lowercasing the pubkey for consistency ([d680f06](https://git.lumeweb.com/LumeWeb/portal/commit/d680f0660f910e323356a1169ee13ef2e647a015))
|
||||
* PostPubkeyChallenge should be using ChallengeRequest ([36745bb](https://git.lumeweb.com/LumeWeb/portal/commit/36745bb55b1d7cd464b085e410333089504591c1))
|
||||
* PostPubkeyChallenge should not be checking email, but pubkey ([db3ba1f](https://git.lumeweb.com/LumeWeb/portal/commit/db3ba1f0148b6abc34b4606f9b8103963a3c6850))
|
||||
* PostPubkeyLogin should be lowercasing the pubkey and signature ([09d53ff](https://git.lumeweb.com/LumeWeb/portal/commit/09d53ffa7645b64aed4170e698b8eb62d2c3590e))
|
||||
* PostPubkeyLogin should not preload any model ([27e7ea7](https://git.lumeweb.com/LumeWeb/portal/commit/27e7ea7d7a0bbf6c147ff625591acf6376c6c62d))
|
||||
* properly handle missing size bytes ([c0df04d](https://git.lumeweb.com/LumeWeb/portal/commit/c0df04d7d5309e32348ceecc68eecd64c5e5cba4))
|
||||
* public_key should be pubkey ([09b9f19](https://git.lumeweb.com/LumeWeb/portal/commit/09b9f195f47ea9ae47069a517a77609c74ea3ca5))
|
||||
* register LoginSession model ([48164ec](https://git.lumeweb.com/LumeWeb/portal/commit/48164ec320c693937ead352246ec1e94bede3684))
|
||||
* register request validation ([c197b14](https://git.lumeweb.com/LumeWeb/portal/commit/c197b1425bbd689e8f662846de0478aff8d38f35))
|
||||
* remove PrivateKey, rename PublicKey in Key model ([00f2b96](https://git.lumeweb.com/LumeWeb/portal/commit/00f2b962a0da956f971dc94d75726c1bab693232))
|
||||
* rewrite gorm query logic for tus uploads ([f8aaeff](https://git.lumeweb.com/LumeWeb/portal/commit/f8aaeff6de2dc5e5321840460d55d79ad1b5ab1a))
|
||||
* rewrite sql logic ([ce1b5e3](https://git.lumeweb.com/LumeWeb/portal/commit/ce1b5e31d5d6a69dc91d88a6fd2f1317e07dc1ea))
|
||||
* rewrite streaming logic and centralize in a helper function ([bb26cfc](https://git.lumeweb.com/LumeWeb/portal/commit/bb26cfca5b4017bbbbf5aeee9bd3577c724f83ca))
|
||||
* save upload info after every chunk ([038d2c4](https://git.lumeweb.com/LumeWeb/portal/commit/038d2c440b24b7c0f1ea72e0bfeda369f766c691))
|
||||
* temp workaround on race condition ([e2db880](https://git.lumeweb.com/LumeWeb/portal/commit/e2db880038f51e0e16ce270fe29fce7785cce878))
|
||||
* **tus:** switch to normal clone package, not generic ([faaec64](https://git.lumeweb.com/LumeWeb/portal/commit/faaec649ead00567ced56edfa9db11eb34655178))
|
||||
* update default flag values ([241db4d](https://git.lumeweb.com/LumeWeb/portal/commit/241db4deb6808d950d55efa38e11d60469cc6778))
|
||||
* update model relationships ([628f1b4](https://git.lumeweb.com/LumeWeb/portal/commit/628f1b4acaac1d2bf373b7008f2e0c070fd64ae5))
|
||||
* **upload:** add account to upload record ([e018a4b](https://git.lumeweb.com/LumeWeb/portal/commit/e018a4b7430bc375ff3b72537e71295cdf67ef93))
|
||||
* uploading of main file ([7aea462](https://git.lumeweb.com/LumeWeb/portal/commit/7aea462ab752e999030837d13733508369524cf3))
|
||||
* upstream renterd updates ([5ad91ad](https://git.lumeweb.com/LumeWeb/portal/commit/5ad91ad263f01830623958141a7e7c8523bee85f))
|
||||
* use AccountID not Account ([f5e4377](https://git.lumeweb.com/LumeWeb/portal/commit/f5e437777a52e2a9bbf55903cea17ec073fbb406))
|
||||
* use bufio reader ([90e4ce6](https://git.lumeweb.com/LumeWeb/portal/commit/90e4ce6408391dc270ca4405a7c5282c2d4766b2))
|
||||
* use challengeObj ([9b82fa7](https://git.lumeweb.com/LumeWeb/portal/commit/9b82fa7828946803289add03fc84be1dc4f86d8b))
|
||||
* use database.path over database.name ([25c7d6d](https://git.lumeweb.com/LumeWeb/portal/commit/25c7d6d4fb48b69239eba131232a78e90a576e2f))
|
||||
* use getWorkerObjectUrl ([4ff1334](https://git.lumeweb.com/LumeWeb/portal/commit/4ff1334d8afd9379db687fc6b764f5b0f1bcc08c))
|
||||
* Use gorm save, and return nil if successful ([26042b6](https://git.lumeweb.com/LumeWeb/portal/commit/26042b62acd7f7346f1a99a0ac37b3f2f99e3f75))
|
||||
* we can't use AddHandler inside BeginRequest ([f941ee4](https://git.lumeweb.com/LumeWeb/portal/commit/f941ee46d469a3f0a6302b188f566029fdec4e70))
|
||||
* wrap Register api in an atomic transaction to avoid dead locks ([e09e51b](https://git.lumeweb.com/LumeWeb/portal/commit/e09e51bb52d513abcbbf53352a5d8ff68eb5364a))
|
||||
* wrong algo ([86380c7](https://git.lumeweb.com/LumeWeb/portal/commit/86380c7b3a97e785b99af456305c01d18f776ddf))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a status endpoint and move cid validation to a utility method ([38b7615](https://git.lumeweb.com/LumeWeb/portal/commit/38b76155af954dc3602a5035cb7b53a7f625fbfd))
|
||||
* add a Status method for uploads ([1f195cf](https://git.lumeweb.com/LumeWeb/portal/commit/1f195cf328ee176be9283ab0cc40e65bb6c40948))
|
||||
* add auth status endpoint ([1dd4fa2](https://git.lumeweb.com/LumeWeb/portal/commit/1dd4fa22cdfc749c5474f94108bca0aec34aea81))
|
||||
* add bao package and rust bao wasm library ([4c649bf](https://git.lumeweb.com/LumeWeb/portal/commit/4c649bfcb92e8632e45cf10b27fa062ff1680c32))
|
||||
* add cid package ([706f7a0](https://git.lumeweb.com/LumeWeb/portal/commit/706f7a05b9a4ed464f693941235aa7e9ca14145a))
|
||||
* add ComputeFile bao RPC method ([687f26c](https://git.lumeweb.com/LumeWeb/portal/commit/687f26cc779f4f50166108d6e78fe1456cfa128d))
|
||||
* add debug mode logging support ([99d7b83](https://git.lumeweb.com/LumeWeb/portal/commit/99d7b8347af25fe65a1f1aecc9960424a101c279))
|
||||
* add download endpoint ([79fd550](https://git.lumeweb.com/LumeWeb/portal/commit/79fd550c54bf74e84d012805f60c036c19fbbef2))
|
||||
* add EncodeString function ([488f873](https://git.lumeweb.com/LumeWeb/portal/commit/488f8737c09b7757c5649b3d8a3568e3c1d5fe45))
|
||||
* add files service with upload endpoint ([b16beeb](https://git.lumeweb.com/LumeWeb/portal/commit/b16beebabb254488897edde870e9588b7be5293e))
|
||||
* add files/upload/limit endpoint ([b77bebe](https://git.lumeweb.com/LumeWeb/portal/commit/b77bebe3b1a03cecdd7e80f575452d5ce91ccfac))
|
||||
* add getCurrentUserId helper function ([29d6db2](https://git.lumeweb.com/LumeWeb/portal/commit/29d6db20096e61efa9a792ef837ef93ca14107ae))
|
||||
* add global cors ([1f5a3d1](https://git.lumeweb.com/LumeWeb/portal/commit/1f5a3d19e44f1db2f8587623e868fa48b23d1a74))
|
||||
* add jwt package ([ea99108](https://git.lumeweb.com/LumeWeb/portal/commit/ea991083276a576003eb3633bd1bde98e13dfe84))
|
||||
* add more validation, and put account creation, with optional pubkey in a transaction ([699e424](https://git.lumeweb.com/LumeWeb/portal/commit/699e4244e0d877d8d9df9d3d4894351785fe7f4d))
|
||||
* add new user service object that implements iris context User interface ([a14dad4](https://git.lumeweb.com/LumeWeb/portal/commit/a14dad43ed3140f73d817ef2438aacbc0939de69))
|
||||
* add newrelic support ([06b3ab8](https://git.lumeweb.com/LumeWeb/portal/commit/06b3ab87f7e1b982d3fb42a3e06897a2fd1387ed))
|
||||
* add pin model ([aaa2c17](https://git.lumeweb.com/LumeWeb/portal/commit/aaa2c17212bd5e646036252a0e1f8d8bdb68f5a7))
|
||||
* add pin service method ([8692a02](https://git.lumeweb.com/LumeWeb/portal/commit/8692a0225ebb71502811cba063e32dd11cdd10c9))
|
||||
* add PostPinBy controller endpoint for pinning a file ([be03a6c](https://git.lumeweb.com/LumeWeb/portal/commit/be03a6c6867f305529af90e6206a0597bb84f015))
|
||||
* add pprof support ([ee17409](https://git.lumeweb.com/LumeWeb/portal/commit/ee17409e1252e9cbae0b17ccbb1949c9a81dff82))
|
||||
* add proof download ([3b1e860](https://git.lumeweb.com/LumeWeb/portal/commit/3b1e860256297d3515f0fcd58dd28292c316d79f))
|
||||
* add StringHash ([118c679](https://git.lumeweb.com/LumeWeb/portal/commit/118c679f769bec2971e4e4b00ec41841a02b8a1c))
|
||||
* add swagger support ([49c3844](https://git.lumeweb.com/LumeWeb/portal/commit/49c38444066c89d7258fd85d114d9d74babb8d55))
|
||||
* add upload model ([f73a04b](https://git.lumeweb.com/LumeWeb/portal/commit/f73a04bb2e48b78e22b531a9121fe4baa011deaf))
|
||||
* add Valid, and Decode methods, and create CID struct ([4e6c29f](https://git.lumeweb.com/LumeWeb/portal/commit/4e6c29f1fd7c33ce442fe741e08b32c8e3e9f393))
|
||||
* add validation to account register ([7257b5d](https://git.lumeweb.com/LumeWeb/portal/commit/7257b5d597a28069c87437cabd71f51c187eb80c))
|
||||
* generate and/or load an ed25519 private key for jwt token generation ([85a0295](https://git.lumeweb.com/LumeWeb/portal/commit/85a02952dffb1873c557f30483606d678e46749d))
|
||||
* initial dnslink support ([cd2f63e](https://git.lumeweb.com/LumeWeb/portal/commit/cd2f63eb72c2bfc404d8d1b5a6fdb53f61a31d1b))
|
||||
* pin file after basic upload ([892f093](https://git.lumeweb.com/LumeWeb/portal/commit/892f093d93348459d113041104d773fdd5124a8d))
|
||||
* pin file after tus upload ([5579ab8](https://git.lumeweb.com/LumeWeb/portal/commit/5579ab85a374be457163d06caf1ac6e260082cca))
|
||||
* tus support ([3005be6](https://git.lumeweb.com/LumeWeb/portal/commit/3005be6fec8136214c1e9480c788f62564a2c5f9))
|
||||
* wip version ([9a4c3d5](https://git.lumeweb.com/LumeWeb/portal/commit/9a4c3d5d13a3e76fe91eb5d78a6f2f0f8e238f80))
|
|
@ -0,0 +1,55 @@
|
|||
# Use the official Node.js image as the base image for building the api/account/portal
|
||||
FROM node:20-alpine as nodejs-builder
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /portal
|
||||
|
||||
# Clone the repository with submodules
|
||||
RUN apk add --no-cache git \
|
||||
&& git clone --recurse-submodules https://git.lumeweb.com/LumeWeb/portal.git -b develop .
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /portal/api/account/app
|
||||
|
||||
# Build the dashboard
|
||||
RUN npm ci && npm run build
|
||||
|
||||
# Use the official Go image as the base image for the final Go build
|
||||
FROM golang:1.21.6-alpine as go-builder
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /portal
|
||||
|
||||
# Build the Go application with configurable tags
|
||||
ARG BUILD_TAGS
|
||||
RUN apk add --no-cache git && git clone --recurse-submodules https://git.lumeweb.com/LumeWeb/portal.git -b develop .
|
||||
|
||||
# Copy the built dashboard from the nodejs-builder stage
|
||||
COPY --from=nodejs-builder /portal/api/account/app/build/client /portal/api/account/app/build/client
|
||||
|
||||
# Install the necessary dependencies
|
||||
RUN apk add bash gcc curl musl-dev
|
||||
|
||||
## Build the Go application
|
||||
RUN go mod download
|
||||
|
||||
## Build the Go application
|
||||
RUN go generate ./...
|
||||
|
||||
## Build the Go application
|
||||
RUN go build -tags "${BUILD_TAGS}" -gcflags="all=-N -l" -o portal ./cmd/portal
|
||||
|
||||
# Use a lightweight base image for the final stage
|
||||
FROM alpine:latest
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /portal
|
||||
|
||||
# Copy the built binary from the go-builder stage
|
||||
COPY --from=go-builder /portal/portal .
|
||||
|
||||
# Expose the necessary port(s)
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the application
|
||||
CMD ["./portal"]
|
|
@ -0,0 +1,731 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/metadata"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/mailer"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/db/models"
|
||||
"go.uber.org/fx"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidOTPCode = errors.New("Invalid OTP code")
|
||||
)
|
||||
|
||||
const ACCOUNT_SUBDOMAIN = "account"
|
||||
|
||||
type AccountServiceParams struct {
|
||||
fx.In
|
||||
Db *gorm.DB
|
||||
Config *config.Manager
|
||||
Identity ed25519.PrivateKey
|
||||
Mailer *mailer.Mailer
|
||||
Metadata metadata.MetadataService
|
||||
}
|
||||
|
||||
var Module = fx.Module("account",
|
||||
fx.Options(
|
||||
fx.Provide(NewAccountService),
|
||||
),
|
||||
)
|
||||
|
||||
type AccountServiceDefault struct {
|
||||
db *gorm.DB
|
||||
config *config.Manager
|
||||
identity ed25519.PrivateKey
|
||||
mailer *mailer.Mailer
|
||||
metadata metadata.MetadataService
|
||||
}
|
||||
|
||||
func NewAccountService(params AccountServiceParams) *AccountServiceDefault {
|
||||
return &AccountServiceDefault{db: params.Db, config: params.Config, identity: params.Identity, mailer: params.Mailer, metadata: params.Metadata}
|
||||
}
|
||||
|
||||
func (s *AccountServiceDefault) EmailExists(email string) (bool, *models.User, error) {
|
||||
user := &models.User{}
|
||||
exists, model, err := s.exists(user, map[string]interface{}{"email": email})
|
||||
if !exists || err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return true, model.(*models.User), nil // Type assertion since `exists` returns interface{}
|
||||
}
|
||||
|
||||
func (s *AccountServiceDefault) PubkeyExists(pubkey string) (bool, *models.PublicKey, error) {
|
||||
publicKey := &models.PublicKey{}
|
||||
exists, model, err := s.exists(publicKey, map[string]interface{}{"key": pubkey})
|
||||
if !exists || err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return true, model.(*models.PublicKey), nil // Type assertion is necessary
|
||||
}
|
||||
|
||||
func (s *AccountServiceDefault) AccountExists(id uint) (bool, *models.User, error) {
|
||||
user := &models.User{}
|
||||
exists, model, err := s.exists(user, map[string]interface{}{"id": id})
|
||||
if !exists || err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return true, model.(*models.User), nil // Ensure to assert the type correctly
|
||||
}
|
||||
|
||||
func (s *AccountServiceDefault) HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", NewAccountError(ErrKeyHashingFailed, err)
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func (s *AccountServiceDefault) CreateAccount(email string, password string, verifyEmail bool) (*models.User, error) {
|
||||
passwordHash, err := s.HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Email: email,
|
||||
PasswordHash: passwordHash,
|
||||
}
|
||||
|
||||
result := s.db.Create(&user)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
|
||||
return nil, NewAccountError(ErrKeyEmailAlreadyExists, nil)
|
||||
}
|
||||
|
||||
if err, ok := result.Error.(*mysql.MySQLError); ok {
|
||||
if err.Number == 1062 {
|
||||
return nil, NewAccountError(ErrKeyEmailAlreadyExists, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, NewAccountError(ErrKeyAccountCreationFailed, result.Error)
|
||||
}
|
||||
|
||||
if verifyEmail {
|
||||
err = s.SendEmailVerification(user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) SendEmailVerification(userId uint) error {
|
||||
exists, user, err := s.AccountExists(userId)
|
||||
if !exists || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Verified {
|
||||
return NewAccountError(ErrKeyAccountAlreadyVerified, nil)
|
||||
}
|
||||
|
||||
token := GenerateSecurityToken()
|
||||
|
||||
var verification models.EmailVerification
|
||||
|
||||
verification.UserID = user.ID
|
||||
verification.Token = token
|
||||
verification.ExpiresAt = time.Now().Add(time.Hour)
|
||||
|
||||
err = s.db.Create(&verification).Error
|
||||
if err != nil {
|
||||
return NewAccountError(ErrKeyDatabaseOperationFailed, err)
|
||||
}
|
||||
|
||||
verifyUrl := fmt.Sprintf("%s/account/verify?token=%s", fmt.Sprintf("https://%s.%s", ACCOUNT_SUBDOMAIN, s.config.Config().Core.Domain), token)
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"FirstName": user.FirstName,
|
||||
"Email": user.Email,
|
||||
"VerificationLink": verifyUrl,
|
||||
"ExpireTime": verification.ExpiresAt.Sub(time.Now()).Round(time.Second * 2),
|
||||
"PortalName": s.config.Config().Core.PortalName,
|
||||
}
|
||||
|
||||
return s.mailer.TemplateSend(mailer.TPL_VERIFY_EMAIL, vars, vars, user.Email)
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) SendPasswordReset(user *models.User) error {
|
||||
token := GenerateSecurityToken()
|
||||
|
||||
var reset models.PasswordReset
|
||||
|
||||
reset.UserID = user.ID
|
||||
reset.Token = token
|
||||
reset.ExpiresAt = time.Now().Add(time.Hour)
|
||||
|
||||
err := s.db.Create(&reset).Error
|
||||
if err != nil {
|
||||
return NewAccountError(ErrKeyDatabaseOperationFailed, err)
|
||||
}
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"FirstName": user.FirstName,
|
||||
"Email": user.Email,
|
||||
"ResetCode": token,
|
||||
"ExpireTime": reset.ExpiresAt,
|
||||
"PortalName": s.config.Config().Core.PortalName,
|
||||
"PortalDomain": s.config.Config().Core.Domain,
|
||||
}
|
||||
|
||||
return s.mailer.TemplateSend(mailer.TPL_PASSWORD_RESET, vars, vars, user.Email)
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) VerifyEmail(email string, token string) error {
|
||||
var verification models.EmailVerification
|
||||
|
||||
verification.Token = token
|
||||
|
||||
result := s.db.Model(&verification).
|
||||
Preload("User").
|
||||
Where(&verification).
|
||||
First(&verification)
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return NewAccountError(ErrKeySecurityInvalidToken, nil)
|
||||
}
|
||||
|
||||
return NewAccountError(ErrKeyDatabaseOperationFailed, nil)
|
||||
}
|
||||
|
||||
if verification.ExpiresAt.Before(time.Now()) {
|
||||
return NewAccountError(ErrKeySecurityTokenExpired, nil)
|
||||
}
|
||||
|
||||
if len(verification.NewEmail) > 0 && verification.NewEmail != email {
|
||||
return NewAccountError(ErrKeySecurityInvalidToken, nil)
|
||||
} else if verification.User.Email != email {
|
||||
return NewAccountError(ErrKeySecurityInvalidToken, nil)
|
||||
}
|
||||
|
||||
var update models.User
|
||||
|
||||
doUpdate := false
|
||||
|
||||
if !verification.User.Verified {
|
||||
update.Verified = true
|
||||
doUpdate = true
|
||||
}
|
||||
|
||||
if len(verification.NewEmail) > 0 {
|
||||
update.Email = verification.NewEmail
|
||||
doUpdate = true
|
||||
}
|
||||
|
||||
if doUpdate {
|
||||
err := s.updateAccountInfo(verification.UserID, update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
verification = models.EmailVerification{
|
||||
UserID: verification.UserID,
|
||||
}
|
||||
|
||||
if result := s.db.Where(&verification).Delete(&verification); result.Error != nil {
|
||||
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) ResetPassword(email string, token string, password string) error {
|
||||
var reset models.PasswordReset
|
||||
|
||||
reset.Token = token
|
||||
|
||||
result := s.db.Model(&reset).
|
||||
Preload("User").
|
||||
Where(&reset).
|
||||
First(&reset)
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return NewAccountError(ErrKeyUserNotFound, result.Error)
|
||||
}
|
||||
|
||||
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
if reset.ExpiresAt.Before(time.Now()) {
|
||||
return NewAccountError(ErrKeySecurityTokenExpired, nil)
|
||||
}
|
||||
|
||||
if reset.User.Email != email {
|
||||
return NewAccountError(ErrKeySecurityInvalidToken, nil)
|
||||
}
|
||||
|
||||
passwordHash, err := s.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.updateAccountInfo(reset.UserID, models.User{PasswordHash: passwordHash})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reset = models.PasswordReset{
|
||||
UserID: reset.UserID,
|
||||
}
|
||||
|
||||
if result := s.db.Where(&reset).Delete(&reset); result.Error != nil {
|
||||
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) UpdateAccountName(userId uint, firstName string, lastName string) error {
|
||||
return s.updateAccountInfo(userId, models.User{FirstName: firstName, LastName: lastName})
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) UpdateAccountEmail(userId uint, email string, password string) error {
|
||||
exists, euser, err := s.EmailExists(email)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) || (exists && euser.ID != userId) {
|
||||
return NewAccountError(ErrKeyEmailAlreadyExists, nil)
|
||||
}
|
||||
|
||||
valid, user, err := s.ValidLoginByUserID(userId, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return NewAccountError(ErrKeyInvalidLogin, nil)
|
||||
}
|
||||
|
||||
if user.Email == email {
|
||||
return NewAccountError(ErrKeyUpdatingSameEmail, nil)
|
||||
}
|
||||
|
||||
var update models.User
|
||||
|
||||
update.Email = email
|
||||
|
||||
return s.updateAccountInfo(userId, update)
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) UpdateAccountPassword(userId uint, password string, newPassword string) error {
|
||||
valid, _, err := s.ValidLoginByUserID(userId, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return NewAccountError(ErrKeyInvalidPassword, nil)
|
||||
}
|
||||
|
||||
passwordHash, err := s.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.updateAccountInfo(userId, models.User{PasswordHash: passwordHash})
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) AddPubkeyToAccount(user models.User, pubkey string) error {
|
||||
var model models.PublicKey
|
||||
|
||||
model.Key = pubkey
|
||||
model.UserID = user.ID
|
||||
|
||||
result := s.db.Create(&model)
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
|
||||
return NewAccountError(ErrKeyPublicKeyExists, result.Error)
|
||||
}
|
||||
|
||||
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (s AccountServiceDefault) LoginPassword(email string, password string, ip string) (string, *models.User, error) {
|
||||
valid, user, err := s.ValidLoginByEmail(email, password)
|
||||
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
token, err := s.doLogin(user, ip, false)
|
||||
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return token, user, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) LoginOTP(userId uint, code string) (string, error) {
|
||||
valid, err := s.OTPVerify(userId, code)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return "", NewAccountError(ErrKeyInvalidOTPCode, nil)
|
||||
}
|
||||
|
||||
var user models.User
|
||||
user.ID = userId
|
||||
|
||||
token, tokenErr := JWTGenerateToken(s.config.Config().Core.Domain, s.identity, user.ID, JWTPurposeLogin)
|
||||
if tokenErr != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) ValidLoginByUserObj(user *models.User, password string) bool {
|
||||
return s.validPassword(user, password)
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) ValidLoginByEmail(email string, password string) (bool, *models.User, error) {
|
||||
var user models.User
|
||||
|
||||
result := s.db.Model(&models.User{}).Where(&models.User{Email: email}).First(&user)
|
||||
|
||||
if result.RowsAffected == 0 || result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return false, nil, NewAccountError(ErrKeyInvalidLogin, result.Error)
|
||||
}
|
||||
|
||||
return false, nil, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
valid := s.ValidLoginByUserObj(&user, password)
|
||||
|
||||
if !valid {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return true, &user, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) ValidLoginByUserID(id uint, password string) (bool, *models.User, error) {
|
||||
var user models.User
|
||||
|
||||
user.ID = id
|
||||
|
||||
result := s.db.Model(&user).Where(&user).First(&user)
|
||||
|
||||
if result.RowsAffected == 0 || result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return false, nil, NewAccountError(ErrKeyInvalidLogin, result.Error)
|
||||
}
|
||||
|
||||
return false, nil, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
valid := s.ValidLoginByUserObj(&user, password)
|
||||
|
||||
if !valid {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return true, &user, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) LoginPubkey(pubkey string, ip string) (string, error) {
|
||||
var model models.PublicKey
|
||||
|
||||
result := s.db.Model(&models.PublicKey{}).Preload("User").Where(&models.PublicKey{Key: pubkey}).First(&model)
|
||||
|
||||
if result.RowsAffected == 0 || result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return "", NewAccountError(ErrKeyInvalidLogin, result.Error)
|
||||
}
|
||||
|
||||
return "", NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
user := model.User
|
||||
|
||||
token, err := s.doLogin(&user, ip, true)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) AccountPins(id uint, createdAfter uint64) ([]models.Pin, error) {
|
||||
var pins []models.Pin
|
||||
|
||||
result := s.db.Model(&models.Pin{}).
|
||||
Preload("Upload"). // Preload the related Upload for each Pin
|
||||
Where(&models.Pin{UserID: id}).
|
||||
Where("created_at > ?", createdAfter).
|
||||
Order("created_at desc").
|
||||
Find(&pins)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, NewAccountError(ErrKeyPinsRetrievalFailed, result.Error)
|
||||
}
|
||||
|
||||
return pins, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) DeletePinByHash(hash []byte, userId uint) error {
|
||||
// Define a struct for the query condition
|
||||
uploadQuery := models.Upload{Hash: hash}
|
||||
|
||||
// Retrieve the upload ID for the given hash
|
||||
var uploadID uint
|
||||
result := s.db.
|
||||
Model(&models.Upload{}).
|
||||
Where(&uploadQuery).
|
||||
Select("id").
|
||||
First(&uploadID)
|
||||
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
// No record found, nothing to delete
|
||||
return nil
|
||||
}
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// Delete pins with the retrieved upload ID and matching account ID
|
||||
pinQuery := models.Pin{UploadID: uploadID, UserID: userId}
|
||||
result = s.db.
|
||||
Where(&pinQuery).
|
||||
Delete(&models.Pin{})
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (s AccountServiceDefault) PinByHash(hash []byte, userId uint) error {
|
||||
// Define a struct for the query condition
|
||||
uploadQuery := models.Upload{Hash: hash}
|
||||
|
||||
result := s.db.
|
||||
Model(&uploadQuery).
|
||||
Where(&uploadQuery).
|
||||
First(&uploadQuery)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return s.PinByID(uploadQuery.ID, userId)
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) PinByID(uploadId uint, userId uint) error {
|
||||
result := s.db.Model(&models.Pin{}).Where(&models.Pin{UploadID: uploadId, UserID: userId}).First(&models.Pin{})
|
||||
|
||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a pin with the retrieved upload ID and matching account ID
|
||||
pinQuery := models.Pin{UploadID: uploadId, UserID: userId}
|
||||
result = s.db.Create(&pinQuery)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) OTPGenerate(userId uint) (string, error) {
|
||||
exists, user, err := s.AccountExists(userId)
|
||||
|
||||
if !exists || err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
otp, otpErr := TOTPGenerate(user.Email, s.config.Config().Core.Domain)
|
||||
if otpErr != nil {
|
||||
return "", NewAccountError(ErrKeyOTPGenerationFailed, otpErr)
|
||||
}
|
||||
|
||||
err = s.updateAccountInfo(user.ID, models.User{OTPSecret: otp})
|
||||
return otp, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) OTPVerify(userId uint, code string) (bool, error) {
|
||||
exists, user, err := s.AccountExists(userId)
|
||||
|
||||
if !exists || err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
valid := TOTPValidate(user.OTPSecret, code)
|
||||
if !valid {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) OTPEnable(userId uint, code string) error {
|
||||
verify, err := s.OTPVerify(userId, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !verify {
|
||||
return ErrInvalidOTPCode
|
||||
}
|
||||
|
||||
return s.updateAccountInfo(userId, models.User{OTPEnabled: true})
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) OTPDisable(userId uint) error {
|
||||
return s.updateAccountInfo(userId, models.User{OTPEnabled: false, OTPSecret: ""})
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) DNSLinkExists(hash []byte) (bool, *models.DNSLink, error) {
|
||||
upload, err := s.metadata.GetUpload(context.Background(), hash)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
exists, model, err := s.exists(&models.DNSLink{}, map[string]interface{}{"upload_id": upload.ID})
|
||||
if !exists || err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
pinned, err := s.UploadPinned(hash)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if !pinned {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return true, model.(*models.DNSLink), nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) UploadPinned(hash []byte) (bool, error) {
|
||||
upload, err := s.metadata.GetUpload(context.Background(), hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var pin models.Pin
|
||||
result := s.db.Model(&models.Pin{}).Where(&models.Pin{UploadID: upload.ID}).First(&pin)
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, result.Error
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func GenerateSecurityToken() string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, 6)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := 0; i < 6; i++ {
|
||||
b[i] = charset[b[i]%byte(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) doLogin(user *models.User, ip string, bypassSecurity bool) (string, error) {
|
||||
purpose := JWTPurposeLogin
|
||||
|
||||
if user.OTPEnabled && !bypassSecurity {
|
||||
purpose = JWTPurpose2FA
|
||||
}
|
||||
|
||||
token, jwtErr := JWTGenerateToken(s.config.Config().Core.Domain, s.identity, user.ID, purpose)
|
||||
if jwtErr != nil {
|
||||
return "", NewAccountError(ErrKeyJWTGenerationFailed, jwtErr)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
err := s.updateAccountInfo(user.ID, models.User{LastLoginIP: ip, LastLogin: &now})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) updateAccountInfo(userId uint, info models.User) error {
|
||||
var user models.User
|
||||
|
||||
user.ID = userId
|
||||
|
||||
result := s.db.Model(&models.User{}).Where(&user).Updates(info)
|
||||
|
||||
if result.Error != nil {
|
||||
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) exists(model interface{}, conditions map[string]interface{}) (bool, interface{}, error) {
|
||||
// Conduct a query with the provided model and conditions
|
||||
result := s.db.Preload(clause.Associations).Model(model).Where(conditions).First(model)
|
||||
|
||||
// Check if any rows were found
|
||||
exists := result.RowsAffected > 0
|
||||
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
if exists {
|
||||
return true, model, nil
|
||||
}
|
||||
|
||||
return false, model, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
|
||||
}
|
||||
|
||||
func (s AccountServiceDefault) validPassword(user *models.User, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
|
||||
|
||||
return err == nil
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// Account creation errors
|
||||
ErrKeyAccountCreationFailed = "ErrAccountCreationFailed"
|
||||
ErrKeyEmailAlreadyExists = "ErrEmailAlreadyExists"
|
||||
ErrKeyUpdatingSameEmail = "ErrUpdatingSameEmail"
|
||||
ErrKeyPasswordHashingFailed = "ErrPasswordHashingFailed"
|
||||
|
||||
// Account lookup and existence verification errors
|
||||
ErrKeyUserNotFound = "ErrUserNotFound"
|
||||
ErrKeyPublicKeyNotFound = "ErrPublicKeyNotFound"
|
||||
|
||||
// Authentication and login errors
|
||||
ErrKeyInvalidLogin = "ErrInvalidLogin"
|
||||
ErrKeyInvalidPassword = "ErrInvalidPassword"
|
||||
ErrKeyInvalidOTPCode = "ErrInvalidOTPCode"
|
||||
ErrKeyOTPVerificationFailed = "ErrOTPVerificationFailed"
|
||||
ErrKeyLoginFailed = "ErrLoginFailed"
|
||||
ErrKeyHashingFailed = "ErrHashingFailed"
|
||||
|
||||
// Account update errors
|
||||
ErrKeyAccountUpdateFailed = "ErrAccountUpdateFailed"
|
||||
ErrKeyAccountAlreadyVerified = "ErrAccountAlreadyVerified"
|
||||
|
||||
// JWT generation errors
|
||||
ErrKeyJWTGenerationFailed = "ErrJWTGenerationFailed"
|
||||
|
||||
// OTP management errors
|
||||
ErrKeyOTPGenerationFailed = "ErrOTPGenerationFailed"
|
||||
ErrKeyOTPEnableFailed = "ErrOTPEnableFailed"
|
||||
ErrKeyOTPDisableFailed = "ErrOTPDisableFailed"
|
||||
|
||||
// Public key management errors
|
||||
ErrKeyAddPublicKeyFailed = "ErrAddPublicKeyFailed"
|
||||
ErrKeyPublicKeyExists = "ErrPublicKeyExists"
|
||||
|
||||
// Pin management errors
|
||||
ErrKeyPinAddFailed = "ErrPinAddFailed"
|
||||
ErrKeyPinDeleteFailed = "ErrPinDeleteFailed"
|
||||
ErrKeyPinsRetrievalFailed = "ErrPinsRetrievalFailed"
|
||||
|
||||
// General errors
|
||||
ErrKeyDatabaseOperationFailed = "ErrDatabaseOperationFailed"
|
||||
|
||||
// Security token errors
|
||||
ErrKeySecurityTokenExpired = "ErrSecurityTokenExpired"
|
||||
ErrKeySecurityInvalidToken = "ErrSecurityInvalidToken"
|
||||
)
|
||||
|
||||
var defaultErrorMessages = map[string]string{
|
||||
// Account creation errors
|
||||
ErrKeyAccountCreationFailed: "Account creation failed due to an internal error.",
|
||||
ErrKeyEmailAlreadyExists: "The email address provided is already in use.",
|
||||
ErrKeyPasswordHashingFailed: "Failed to secure the password, please try again later.",
|
||||
ErrKeyUpdatingSameEmail: "The email address provided is the same as your current one.",
|
||||
|
||||
// Account lookup and existence verification errors
|
||||
ErrKeyUserNotFound: "The requested user was not found.",
|
||||
ErrKeyPublicKeyNotFound: "The specified public key was not found.",
|
||||
ErrKeyHashingFailed: "Failed to hash the password.",
|
||||
|
||||
// Authentication and login errors
|
||||
ErrKeyInvalidLogin: "The login credentials provided are invalid.",
|
||||
ErrKeyInvalidPassword: "The password provided is incorrect.",
|
||||
ErrKeyInvalidOTPCode: "The OTP code provided is invalid or expired.",
|
||||
ErrKeyOTPVerificationFailed: "OTP verification failed, please try again.",
|
||||
ErrKeyLoginFailed: "Login failed due to an internal error.",
|
||||
|
||||
// Account update errors
|
||||
ErrKeyAccountUpdateFailed: "Failed to update account information.",
|
||||
ErrKeyAccountAlreadyVerified: "Account is already verified.",
|
||||
|
||||
// JWT generation errors
|
||||
ErrKeyJWTGenerationFailed: "Failed to generate a new JWT token.",
|
||||
|
||||
// OTP management errors
|
||||
ErrKeyOTPGenerationFailed: "Failed to generate a new OTP secret.",
|
||||
ErrKeyOTPEnableFailed: "Enabling OTP authentication failed.",
|
||||
ErrKeyOTPDisableFailed: "Disabling OTP authentication failed.",
|
||||
|
||||
// Public key management errors
|
||||
ErrKeyAddPublicKeyFailed: "Adding the public key to the account failed.",
|
||||
ErrKeyPublicKeyExists: "The public key already exists for this account.",
|
||||
|
||||
// Pin management errors
|
||||
ErrKeyPinAddFailed: "Failed to add the pin.",
|
||||
ErrKeyPinDeleteFailed: "Failed to delete the pin.",
|
||||
ErrKeyPinsRetrievalFailed: "Failed to retrieve pins.",
|
||||
|
||||
// General errors
|
||||
ErrKeyDatabaseOperationFailed: "A database operation failed.",
|
||||
|
||||
// Security token errors
|
||||
ErrKeySecurityTokenExpired: "The security token has expired.",
|
||||
ErrKeySecurityInvalidToken: "The security token is invalid.",
|
||||
}
|
||||
|
||||
var (
|
||||
ErrorCodeToHttpStatus = map[string]int{
|
||||
// Account creation errors
|
||||
ErrKeyAccountCreationFailed: http.StatusInternalServerError,
|
||||
ErrKeyEmailAlreadyExists: http.StatusConflict,
|
||||
ErrKeyPasswordHashingFailed: http.StatusInternalServerError,
|
||||
|
||||
// Account lookup and existence verification errors
|
||||
ErrKeyUserNotFound: http.StatusNotFound,
|
||||
ErrKeyPublicKeyNotFound: http.StatusNotFound,
|
||||
|
||||
// Authentication and login errors
|
||||
ErrKeyInvalidLogin: http.StatusUnauthorized,
|
||||
ErrKeyInvalidPassword: http.StatusUnauthorized,
|
||||
ErrKeyInvalidOTPCode: http.StatusBadRequest,
|
||||
ErrKeyOTPVerificationFailed: http.StatusBadRequest,
|
||||
ErrKeyLoginFailed: http.StatusInternalServerError,
|
||||
|
||||
// Account update errors
|
||||
ErrKeyAccountUpdateFailed: http.StatusInternalServerError,
|
||||
ErrKeyAccountAlreadyVerified: http.StatusConflict,
|
||||
|
||||
// JWT generation errors
|
||||
ErrKeyJWTGenerationFailed: http.StatusInternalServerError,
|
||||
|
||||
// OTP management errors
|
||||
ErrKeyOTPGenerationFailed: http.StatusInternalServerError,
|
||||
ErrKeyOTPEnableFailed: http.StatusInternalServerError,
|
||||
ErrKeyOTPDisableFailed: http.StatusInternalServerError,
|
||||
|
||||
// Public key management errors
|
||||
ErrKeyAddPublicKeyFailed: http.StatusInternalServerError,
|
||||
ErrKeyPublicKeyExists: http.StatusConflict,
|
||||
|
||||
// Pin management errors
|
||||
ErrKeyPinAddFailed: http.StatusInternalServerError,
|
||||
ErrKeyPinDeleteFailed: http.StatusInternalServerError,
|
||||
ErrKeyPinsRetrievalFailed: http.StatusInternalServerError,
|
||||
|
||||
// General errors
|
||||
ErrKeyDatabaseOperationFailed: http.StatusInternalServerError,
|
||||
ErrKeyHashingFailed: http.StatusInternalServerError,
|
||||
|
||||
// Security token errors
|
||||
ErrKeySecurityTokenExpired: http.StatusUnauthorized,
|
||||
ErrKeySecurityInvalidToken: http.StatusUnauthorized,
|
||||
}
|
||||
)
|
||||
|
||||
type AccountError struct {
|
||||
Key string // A unique identifier for the error type
|
||||
Message string // Human-readable error message
|
||||
Err error // Underlying error, if any
|
||||
}
|
||||
|
||||
func (e *AccountError) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewAccountError(key string, err error, customMessage ...string) *AccountError {
|
||||
message, exists := defaultErrorMessages[key]
|
||||
if !exists {
|
||||
message = "An unknown error occurred"
|
||||
}
|
||||
if len(customMessage) > 0 {
|
||||
message = customMessage[0]
|
||||
}
|
||||
return &AccountError{
|
||||
Key: key,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"go.sia.tech/jape"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/api/router"
|
||||
|
||||
apiRegistry "git.lumeweb.com/LumeWeb/portal/api/registry"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const AUTH_COOKIE_NAME = "auth_token"
|
||||
|
||||
type JWTPurpose string
|
||||
type VerifyTokenFunc func(claim *jwt.RegisteredClaims) error
|
||||
|
||||
var (
|
||||
nopVerifyFunc VerifyTokenFunc = func(claim *jwt.RegisteredClaims) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ErrJWTUnexpectedClaimsType = errors.New("unexpected claims type")
|
||||
ErrJWTUnexpectedIssuer = errors.New("unexpected issuer")
|
||||
ErrJWTInvalid = errors.New("invalid JWT")
|
||||
)
|
||||
|
||||
const (
|
||||
JWTPurposeLogin JWTPurpose = "login"
|
||||
JWTPurpose2FA JWTPurpose = "2fa"
|
||||
JWTPurposeNone JWTPurpose = ""
|
||||
)
|
||||
|
||||
func JWTGenerateToken(domain string, privateKey ed25519.PrivateKey, userID uint, purpose JWTPurpose) (string, error) {
|
||||
return JWTGenerateTokenWithDuration(domain, privateKey, userID, time.Hour*24, purpose)
|
||||
}
|
||||
|
||||
func JWTGenerateTokenWithDuration(domain string, privateKey ed25519.PrivateKey, userID uint, duration time.Duration, purpose JWTPurpose) (string, error) {
|
||||
|
||||
// Define the claims
|
||||
claims := jwt.RegisteredClaims{
|
||||
Issuer: domain,
|
||||
Subject: strconv.Itoa(int(userID)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Audience: []string{string(purpose)},
|
||||
}
|
||||
|
||||
// Create the token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
|
||||
|
||||
// Sign the token with the Ed25519 private key
|
||||
tokenString, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func JWTVerifyToken(token string, domain string, privateKey ed25519.PrivateKey, verifyFunc VerifyTokenFunc) (*jwt.RegisteredClaims, error) {
|
||||
validatedToken, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
publicKey := privateKey.Public()
|
||||
|
||||
return publicKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if verifyFunc == nil {
|
||||
verifyFunc = nopVerifyFunc
|
||||
}
|
||||
|
||||
claim, ok := validatedToken.Claims.(*jwt.RegisteredClaims)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrJWTUnexpectedClaimsType, validatedToken.Claims)
|
||||
}
|
||||
|
||||
if domain != claim.Issuer {
|
||||
return nil, fmt.Errorf("%w: %s", ErrJWTUnexpectedIssuer, claim.Issuer)
|
||||
}
|
||||
|
||||
err = verifyFunc(claim)
|
||||
|
||||
return claim, err
|
||||
}
|
||||
|
||||
func SetAuthCookie(jc jape.Context, c *config.Manager, jwt string) {
|
||||
|
||||
for _, api := range apiRegistry.GetAllAPIs() {
|
||||
routeableApi, ok := api.(router.RoutableAPI)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
http.SetCookie(jc.ResponseWriter, &http.Cookie{
|
||||
Name: routeableApi.AuthTokenName(),
|
||||
Value: jwt,
|
||||
MaxAge: int((24 * time.Hour).Seconds()),
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Domain: c.Config().Core.Domain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func EchoAuthCookie(jc jape.Context, config *config.Manager) {
|
||||
for _, api := range apiRegistry.GetAllAPIs() {
|
||||
routeableApi, ok := api.(router.RoutableAPI)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
cookies := lo.Filter(jc.Request.Cookies(), func(item *http.Cookie, _ int) bool {
|
||||
return item.Name == routeableApi.AuthTokenName()
|
||||
})
|
||||
|
||||
if len(cookies) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
unverified, _, err := jwt.NewParser().ParseUnverified(cookies[0].Value, &jwt.RegisteredClaims{})
|
||||
if err != nil {
|
||||
http.Error(jc.ResponseWriter, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
exp, err := unverified.Claims.GetExpirationTime()
|
||||
if err != nil {
|
||||
http.Error(jc.ResponseWriter, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(jc.ResponseWriter, &http.Cookie{
|
||||
Name: cookies[0].Name,
|
||||
Value: cookies[0].Value,
|
||||
MaxAge: int(exp.Time.Sub(time.Now()).Seconds()),
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Domain: config.Config().Core.Domain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ClearAuthCookie(jc jape.Context, config *config.Manager) {
|
||||
for _, api := range apiRegistry.GetAllAPIs() {
|
||||
routeableApi, ok := api.(router.RoutableAPI)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
jc.ResponseWriter.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
jc.ResponseWriter.Header().Set("Pragma", "no-cache")
|
||||
jc.ResponseWriter.Header().Set("Expires", "0")
|
||||
|
||||
http.SetCookie(jc.ResponseWriter, &http.Cookie{
|
||||
Name: routeableApi.AuthTokenName(),
|
||||
Value: "",
|
||||
Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
MaxAge: -1,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Domain: config.Config().Core.Domain,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package account
|
||||
|
||||
import "github.com/pquerna/otp/totp"
|
||||
|
||||
func TOTPGenerate(domain string, email string) (string, error) {
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: domain,
|
||||
AccountName: email,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return key.Secret(), nil
|
||||
}
|
||||
|
||||
func TOTPValidate(secret string, code string) bool {
|
||||
return totp.Validate(code, secret)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package account
|
||||
|
||||
import "go.sia.tech/jape"
|
||||
|
||||
func SendJWT(jc jape.Context, jwt string) {
|
||||
jc.ResponseWriter.Header().Set("Authorization", "Bearer "+jwt)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/api/account"
|
||||
"git.lumeweb.com/LumeWeb/portal/api/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.RegisterEntry(registry.APIEntry{
|
||||
Key: "account",
|
||||
Module: account.Module,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,509 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/cors"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/api/swagger"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/api/router"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/account"
|
||||
"git.lumeweb.com/LumeWeb/portal/api/middleware"
|
||||
"git.lumeweb.com/LumeWeb/portal/api/registry"
|
||||
"go.sia.tech/jape"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
//go:embed swagger.yaml
|
||||
var swagSpec []byte
|
||||
|
||||
//go:embed all:app/build/client
|
||||
var appFs embed.FS
|
||||
|
||||
var (
|
||||
_ registry.API = (*AccountAPI)(nil)
|
||||
_ router.RoutableAPI = (*AccountAPI)(nil)
|
||||
)
|
||||
|
||||
type AccountAPI struct {
|
||||
config *config.Manager
|
||||
accounts *account.AccountServiceDefault
|
||||
identity ed25519.PrivateKey
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type AccountAPIParams struct {
|
||||
fx.In
|
||||
Config *config.Manager
|
||||
Accounts *account.AccountServiceDefault
|
||||
Identity ed25519.PrivateKey
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewS5(params AccountAPIParams) AccountApiResult {
|
||||
api := &AccountAPI{
|
||||
config: params.Config,
|
||||
accounts: params.Accounts,
|
||||
identity: params.Identity,
|
||||
logger: params.Logger,
|
||||
}
|
||||
|
||||
return AccountApiResult{
|
||||
API: api,
|
||||
AccountAPI: api,
|
||||
}
|
||||
}
|
||||
|
||||
var Module = fx.Module("s5_api",
|
||||
fx.Provide(NewS5),
|
||||
)
|
||||
|
||||
type AccountApiResult struct {
|
||||
fx.Out
|
||||
API registry.API `group:"api"`
|
||||
AccountAPI *AccountAPI
|
||||
}
|
||||
|
||||
func (a AccountAPI) Name() string {
|
||||
return "account"
|
||||
}
|
||||
|
||||
func (a *AccountAPI) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AccountAPI) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AccountAPI) Stop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AccountAPI) login(jc jape.Context) {
|
||||
var request LoginRequest
|
||||
|
||||
if jc.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exists, _, err := a.accounts.EmailExists(request.Email)
|
||||
|
||||
if !exists {
|
||||
_ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, nil), http.StatusUnauthorized)
|
||||
if err != nil {
|
||||
a.logger.Error("failed to check if email exists", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
jwt, user, err := a.accounts.LoginPassword(request.Email, request.Password, jc.Request.RemoteAddr)
|
||||
if err != nil || user == nil {
|
||||
_ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, err), http.StatusUnauthorized)
|
||||
if err != nil {
|
||||
a.logger.Error("failed to login", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
account.SetAuthCookie(jc, a.config, jwt)
|
||||
account.SendJWT(jc, jwt)
|
||||
|
||||
jc.Encode(&LoginResponse{
|
||||
Token: jwt,
|
||||
Otp: user.OTPEnabled && user.OTPVerified,
|
||||
})
|
||||
}
|
||||
|
||||
func (a AccountAPI) register(jc jape.Context) {
|
||||
var request RegisterRequest
|
||||
|
||||
if jc.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(request.FirstName) == 0 || len(request.LastName) == 0 {
|
||||
_ = jc.Error(account.NewAccountError(account.ErrKeyAccountCreationFailed, nil), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := a.accounts.CreateAccount(request.Email, request.Password, true)
|
||||
if err != nil {
|
||||
_ = jc.Error(err, http.StatusUnauthorized)
|
||||
a.logger.Error("failed to update account name", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = a.accounts.UpdateAccountName(user.ID, request.FirstName, request.LastName)
|
||||
|
||||
if err != nil {
|
||||
_ = jc.Error(account.NewAccountError(account.ErrKeyAccountCreationFailed, err), http.StatusBadRequest)
|
||||
a.logger.Error("failed to update account name", zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a AccountAPI) verifyEmail(jc jape.Context) {
|
||||
var request VerifyEmailRequest
|
||||
|
||||
if jc.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if request.Email == "" || request.Token == "" {
|
||||
_ = jc.Error(errors.New("invalid request"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := a.accounts.VerifyEmail(request.Email, request.Token)
|
||||
|
||||
if jc.Check("Failed to verify email", err) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a AccountAPI) resendVerifyEmail(jc jape.Context) {
|
||||
user := middleware.GetUserFromContext(jc.Request.Context())
|
||||
|
||||
err := a.accounts.SendEmailVerification(user)
|
||||
|
||||
if jc.Check("failed to resend email verification", err) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a AccountAPI) otpGenerate(jc jape.Context) {
|
||||
user := middleware.GetUserFromContext(jc.Request.Context())
|
||||
|
||||
otp, err := a.accounts.OTPGenerate(user)
|
||||
if jc.Check("failed to generate otp", err) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
jc.Encode(&OTPGenerateResponse{
|
||||
OTP: otp,
|
||||
})
|
||||
}
|
||||
|
||||
func (a AccountAPI) otpVerify(jc jape.Context) {
|
||||
user := middleware.GetUserFromContext(jc.Request.Context())
|
||||
|
||||
var request OTPVerifyRequest
|
||||
|
||||
if jc.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.accounts.OTPEnable(user, request.OTP)
|
||||
|
||||
if jc.Check("failed to verify otp", err) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a AccountAPI) otpValidate(jc jape.Context) {
|
||||
user := middleware.GetUserFromContext(jc.Request.Context())
|
||||
|
||||
var request OTPValidateRequest
|
||||
|
||||
if jc.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := a.accounts.LoginOTP(user, request.OTP)
|
||||
if jc.Check("failed to validate otp", err) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
account.SetAuthCookie(jc, a.config, jwt)
|
||||
account.SendJWT(jc, jwt)
|
||||
|
||||
jc.Encode(&LoginResponse{
|
||||
Token: jwt,
|
||||
Otp: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (a AccountAPI) otpDisable(jc jape.Context) {
|
||||
user := middleware.GetUserFromContext(jc.Request.Context())
|
||||
|
||||
var request OTPDisableRequest
|
||||
|
||||
if jc.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
valid, _, err := a.accounts.ValidLoginByUserID(user, request.Password)
|
||||
|
||||
if !valid {
|
||||
_ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, nil), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.accounts.OTPDisable(user)
|
||||
if jc.Check("failed to disable otp", err) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a AccountAPI) passwordResetRequest(jc jape.Context) {
|
||||
var request PasswordResetRequest
|
||||
|
||||
if jc.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exists, user, err := a.accounts.EmailExists(request.Email)
|
||||
if jc.Check("invalid request", err) != nil || !exists {
|
||||
return
|
||||
}
|
||||
|
||||
err = a.accounts.SendPasswordReset(user)
|
||||
if jc.Check("failed to request password reset", err) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
jc.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a AccountAPI) passwordResetConfirm(jc jape.Context) {
|
||||
var request PasswordResetVerifyRequest
|
||||
|
||||
if jc.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exists, _, err := a.accounts.EmailExists(request.Email)
|
||||
if jc.Check("invalid request", err) != nil || !exists {
|
||||
return
|
||||
}
|
||||
|
||||
err = a.accounts.ResetPassword(request.Email, request.Password, request.Token)
|
||||
if jc.Check("failed to reset password", err) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
jc.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a AccountAPI) ping(jc jape.Context) {
|
||||
token := middleware.GetAuthTokenFromContext(jc.Request.Context())
|
||||
account.EchoAuthCookie(jc, a.config)
|
||||
jc.Encode(&PongResponse{
|
||||
Ping: "pong",
|
||||
Token: token,
|
||||
})
|
||||
}
|
||||
|
||||
func (a AccountAPI) accountInfo(jc jape.Context) {
|
||||
user := middleware.GetUserFromContext(jc.Request.Context())
|
||||
|
||||
_, acct, _ := a.accounts.AccountExists(user)
|
||||
|
||||
jc.Encode(&AccountInfoResponse{
|
||||
ID: acct.ID,
|
||||
Email: acct.Email,
|
||||
FirstName: acct.FirstName,
|
||||
LastName: acct.LastName,
|
||||
Verified: acct.Verified,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (a AccountAPI) logout(c jape.Context) {
|
||||
account.ClearAuthCookie(c, a.config)
|
||||
}
|
||||
|
||||
func (a AccountAPI) uploadLimit(c jape.Context) {
|
||||
c.Encode(&UploadLimitResponse{
|
||||
Limit: a.config.Config().Core.PostUploadLimit,
|
||||
})
|
||||
}
|
||||
|
||||
func (a AccountAPI) updateEmail(c jape.Context) {
|
||||
user := middleware.GetUserFromContext(c.Request.Context())
|
||||
|
||||
var request UpdateEmailRequest
|
||||
|
||||
if c.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.accounts.UpdateAccountEmail(user, request.Email, request.Password)
|
||||
if c.Check("failed to update email", err) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a AccountAPI) updatePassword(c jape.Context) {
|
||||
user := middleware.GetUserFromContext(c.Request.Context())
|
||||
|
||||
var request UpdatePasswordRequest
|
||||
|
||||
if c.Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.accounts.UpdateAccountPassword(user, request.CurrentPassword, request.NewPassword)
|
||||
if c.Check("failed to update password", err) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (a AccountAPI) meta(c jape.Context) {
|
||||
c.Encode(&MetaResponse{
|
||||
Domain: a.config.Config().Core.Domain,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (a *AccountAPI) Routes() (*httprouter.Router, error) {
|
||||
loginAuthMw2fa := authMiddleware(middleware.AuthMiddlewareOptions{
|
||||
Identity: a.identity,
|
||||
Accounts: a.accounts,
|
||||
Config: a.config,
|
||||
Purpose: account.JWTPurpose2FA,
|
||||
EmptyAllowed: true,
|
||||
ExpiredAllowed: true,
|
||||
})
|
||||
|
||||
authMw := authMiddleware(middleware.AuthMiddlewareOptions{
|
||||
Identity: a.identity,
|
||||
Accounts: a.accounts,
|
||||
Config: a.config,
|
||||
Purpose: account.JWTPurposeNone,
|
||||
})
|
||||
|
||||
pingAuthMw := authMiddleware(middleware.AuthMiddlewareOptions{
|
||||
Identity: a.identity,
|
||||
Accounts: a.accounts,
|
||||
Config: a.config,
|
||||
Purpose: account.JWTPurposeLogin,
|
||||
})
|
||||
|
||||
appFiles, _ := fs.Sub(appFs, "app/build/client")
|
||||
appServ := http.FileServer(http.FS(appFiles))
|
||||
|
||||
appHandler := func(c jape.Context) {
|
||||
appServ.ServeHTTP(c.ResponseWriter, c.Request)
|
||||
}
|
||||
|
||||
appServer := middleware.ApplyMiddlewares(appHandler, middleware.ProxyMiddleware)
|
||||
|
||||
swaggerRoutes, err := swagger.Swagger(swagSpec, map[string]jape.Handler{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swaggerJape := jape.Mux(swaggerRoutes)
|
||||
|
||||
getApiJape := jape.Mux(map[string]jape.Handler{
|
||||
"GET /api/auth/otp/generate": middleware.ApplyMiddlewares(a.otpGenerate, authMw, middleware.ProxyMiddleware),
|
||||
"GET /api/account": middleware.ApplyMiddlewares(a.accountInfo, authMw, middleware.ProxyMiddleware),
|
||||
"GET /api/upload-limit": middleware.ApplyMiddlewares(a.uploadLimit, middleware.ProxyMiddleware),
|
||||
"GET /api/meta": middleware.ApplyMiddlewares(a.meta, middleware.ProxyMiddleware),
|
||||
})
|
||||
|
||||
getHandler := func(c jape.Context) {
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/api") {
|
||||
getApiJape.ServeHTTP(c.ResponseWriter, c.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/swagger") {
|
||||
swaggerJape.ServeHTTP(c.ResponseWriter, c.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(c.Request.URL.Path, "/assets") && c.Request.URL.Path != "favicon.ico" && c.Request.URL.Path != "/" && !strings.HasSuffix(c.Request.URL.Path, ".html") {
|
||||
c.Request.URL.Path = "/"
|
||||
}
|
||||
appServer(c)
|
||||
}
|
||||
|
||||
corsMw := cors.New(cors.Options{
|
||||
AllowOriginFunc: func(origin string) bool {
|
||||
return true
|
||||
},
|
||||
AllowedMethods: []string{"GET", "POST", "DELETE"},
|
||||
AllowedHeaders: []string{"Authorization", "Content-Type"},
|
||||
})
|
||||
|
||||
corsOptionsHandler := func(c jape.Context) {
|
||||
c.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
routes := map[string]jape.Handler{
|
||||
// Auth
|
||||
"POST /api/auth/ping": middleware.ApplyMiddlewares(a.ping, corsMw.Handler, pingAuthMw, middleware.ProxyMiddleware),
|
||||
"POST /api/auth/login": middleware.ApplyMiddlewares(a.login, corsMw.Handler, loginAuthMw2fa, middleware.ProxyMiddleware),
|
||||
"POST /api/auth/register": middleware.ApplyMiddlewares(a.register, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"POST /api/auth/otp/validate": middleware.ApplyMiddlewares(a.otpValidate, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"POST /api/auth/logout": middleware.ApplyMiddlewares(a.logout, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
|
||||
// Account
|
||||
"POST /api/account/verify-email": middleware.ApplyMiddlewares(a.verifyEmail, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"POST /api/account/verify-email/resend": middleware.ApplyMiddlewares(a.resendVerifyEmail, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"POST /api/account/otp/verify": middleware.ApplyMiddlewares(a.otpVerify, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"POST /api/account/otp/disable": middleware.ApplyMiddlewares(a.otpDisable, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"POST /api/account/password-reset/request": middleware.ApplyMiddlewares(a.passwordResetRequest, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"POST /api/account/password-reset/confirm": middleware.ApplyMiddlewares(a.passwordResetConfirm, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"POST /api/account/update-email": middleware.ApplyMiddlewares(a.updateEmail, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"POST /api/account/update-password": middleware.ApplyMiddlewares(a.updatePassword, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
|
||||
// CORS
|
||||
"OPTIONS /api/auth/ping": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/auth/login": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, loginAuthMw2fa, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/auth/register": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/auth/otp/validate": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/auth/logout": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
|
||||
"OPTIONS /api/account/verify-email": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/account/verify-email/resend": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/account/otp/verify": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/account/otp/disable": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/account/password-reset/request": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/account/password-reset/confirm": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/account/update-email": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/account/update-password": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
|
||||
// Get Routes
|
||||
"OPTIONS /api/upload-limit": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/account": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
"OPTIONS /api/auth/otp/generate": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
|
||||
|
||||
"GET /*path": middleware.ApplyMiddlewares(getHandler, corsMw.Handler),
|
||||
}
|
||||
|
||||
return jape.Mux(routes), nil
|
||||
}
|
||||
func (a AccountAPI) Can(w http.ResponseWriter, r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a AccountAPI) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (a *AccountAPI) Domain() string {
|
||||
return router.BuildSubdomain(a, a.config)
|
||||
}
|
||||
|
||||
func (a AccountAPI) AuthTokenName() string {
|
||||
return account.AUTH_COOKIE_NAME
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Subproject commit d7f0154fb89dc9dfafc2bc7fd22e24c6ebaaafc7
|
|
@ -0,0 +1,73 @@
|
|||
package account
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
Otp bool `json:"otp"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type OTPGenerateResponse struct {
|
||||
OTP string `json:"otp"`
|
||||
}
|
||||
|
||||
type OTPVerifyRequest struct {
|
||||
OTP string `json:"otp"`
|
||||
}
|
||||
|
||||
type OTPValidateRequest struct {
|
||||
OTP string `json:"otp"`
|
||||
}
|
||||
type OTPDisableRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
type VerifyEmailRequest struct {
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
type PasswordResetRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
type PasswordResetVerifyRequest struct {
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type PongResponse struct {
|
||||
Ping string `json:"ping"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
type AccountInfoResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
type UploadLimitResponse struct {
|
||||
Limit uint64 `json:"limit"`
|
||||
}
|
||||
type UpdateEmailRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
type UpdatePasswordRequest struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
type MetaResponse struct {
|
||||
Domain string `json:"domain"`
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/account"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/api/middleware"
|
||||
)
|
||||
|
||||
const (
|
||||
authCookieName = account.AUTH_COOKIE_NAME
|
||||
authQueryParam = "auth_token"
|
||||
)
|
||||
|
||||
func findToken(r *http.Request) string {
|
||||
return middleware.FindAuthToken(r, authCookieName, authQueryParam)
|
||||
}
|
||||
|
||||
func authMiddleware(options middleware.AuthMiddlewareOptions) middleware.HttpMiddlewareFunc {
|
||||
options.FindToken = findToken
|
||||
return middleware.AuthMiddleware(options)
|
||||
}
|
|
@ -0,0 +1,351 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Account Management API
|
||||
version: "1.0"
|
||||
description: API for managing user accounts, including login, registration, OTP operations, and password resets.
|
||||
paths:
|
||||
/api/auth/login:
|
||||
post:
|
||||
summary: Login to the system
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully logged in
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginResponse'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
/api/auth/logout:
|
||||
post:
|
||||
summary: Logout of account service
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully logged out
|
||||
/api/auth/register:
|
||||
post:
|
||||
summary: Register a new account
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully registered
|
||||
'400':
|
||||
description: Bad Request
|
||||
/api/account/verify-email:
|
||||
post:
|
||||
summary: Verify email address
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/VerifyEmailRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Email verified successfully
|
||||
/api/account/verify-email/resend:
|
||||
post:
|
||||
summary: Resend email verification
|
||||
responses:
|
||||
'200':
|
||||
description: Email verification resent successfully
|
||||
/api/auth/otp/generate:
|
||||
get:
|
||||
summary: Generate OTP for two-factor authentication
|
||||
responses:
|
||||
'200':
|
||||
description: OTP generated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OTPGenerateResponse'
|
||||
/api/account/otp/verify:
|
||||
post:
|
||||
summary: Verify OTP for enabling two-factor authentication
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OTPVerifyRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: OTP verified successfully
|
||||
/api/account/otp/validate:
|
||||
post:
|
||||
summary: Validate OTP for two-factor authentication login
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OTPValidateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: OTP validated successfully
|
||||
/api/auth/otp/disable:
|
||||
post:
|
||||
summary: Disable OTP for two-factor authentication
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OTPDisableRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: OTP disabled successfully
|
||||
/api/account/password-reset/request:
|
||||
post:
|
||||
summary: Request a password reset
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PasswordResetRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Password reset requested successfully
|
||||
/api/account/password-reset/confirm:
|
||||
post:
|
||||
summary: Confirm a password reset
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PasswordResetVerifyRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Password reset successfully
|
||||
/api/auth/ping:
|
||||
post:
|
||||
summary: Auth check endpoint
|
||||
responses:
|
||||
'200':
|
||||
description: Pong
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PingResponse'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
/api/account:
|
||||
get:
|
||||
summary: Get account information
|
||||
responses:
|
||||
'200':
|
||||
description: Account information retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountInfoResponse'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
/api/account/update-email:
|
||||
post:
|
||||
summary: Update email address
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateEmailRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Email updated successfully
|
||||
/api/account/update-password:
|
||||
post:
|
||||
summary: Update password
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdatePasswordRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Password updated successfully
|
||||
/api/upload-limit:
|
||||
get:
|
||||
summary: Get the basic file upload (POST) upload limit set by the portal
|
||||
responses:
|
||||
'200':
|
||||
description: Upload limit retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UploadLimitResponse'
|
||||
/api/meta:
|
||||
get:
|
||||
summary: Get metadata about the portal
|
||||
responses:
|
||||
'200':
|
||||
description: Metadata retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MetaResponse'
|
||||
|
||||
|
||||
components:
|
||||
schemas:
|
||||
LoginRequest:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
LoginResponse:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required:
|
||||
- first_name
|
||||
- last_name
|
||||
- email
|
||||
- password
|
||||
properties:
|
||||
first_name:
|
||||
type: string
|
||||
last_name:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
VerifyEmailRequest:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
- token
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
OTPGenerateResponse:
|
||||
type: object
|
||||
properties:
|
||||
OTP:
|
||||
type: string
|
||||
OTPVerifyRequest:
|
||||
type: object
|
||||
required:
|
||||
- OTP
|
||||
properties:
|
||||
OTP:
|
||||
type: string
|
||||
OTPValidateRequest:
|
||||
type: object
|
||||
required:
|
||||
- OTP
|
||||
properties:
|
||||
OTP:
|
||||
type: string
|
||||
OTPDisableRequest:
|
||||
type: object
|
||||
required:
|
||||
- password
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
PasswordResetRequest:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
PasswordResetVerifyRequest:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
- token
|
||||
- password
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
UpdateEmailRequest:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
UpdatePasswordRequest:
|
||||
type: object
|
||||
required:
|
||||
- current_password
|
||||
- new_password
|
||||
properties:
|
||||
current_password:
|
||||
type: string
|
||||
new_password:
|
||||
type: string
|
||||
PingResponse:
|
||||
type: object
|
||||
properties:
|
||||
ping:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
AccountInfoResponse:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- first_name
|
||||
- last_name
|
||||
- email
|
||||
- verified
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
first_name:
|
||||
type: string
|
||||
last_name:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
verified:
|
||||
type: boolean
|
||||
UploadLimitResponse:
|
||||
type: object
|
||||
properties:
|
||||
limit:
|
||||
type: number
|
||||
required:
|
||||
- limit
|
||||
MetaResponse:
|
||||
type: object
|
||||
required:
|
||||
- domain
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
|
@ -0,0 +1,68 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/api/registry"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
var alwaysEnabled = []string{"account"}
|
||||
|
||||
func BuildApis(cm *config.Manager) fx.Option {
|
||||
var options []fx.Option
|
||||
enabledProtocols := cm.Viper().GetStringSlice("core.protocols")
|
||||
for _, entry := range registry.GetEntryRegistry() {
|
||||
if slices.Contains(enabledProtocols, entry.Key) || slices.Contains(alwaysEnabled, entry.Key) {
|
||||
options = append(options, entry.Module)
|
||||
}
|
||||
}
|
||||
|
||||
type initParams struct {
|
||||
fx.In
|
||||
Apis []registry.API `group:"api"`
|
||||
}
|
||||
|
||||
options = append(options, fx.Invoke(func(params initParams) error {
|
||||
for _, protocol := range params.Apis {
|
||||
err := protocol.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registry.RegisterAPI(protocol)
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
return fx.Module("api", fx.Options(options...))
|
||||
}
|
||||
|
||||
type LifecyclesParams struct {
|
||||
fx.In
|
||||
|
||||
Protocols []registry.API `group:"protocol"`
|
||||
}
|
||||
|
||||
func SetupLifecycles(lifecycle fx.Lifecycle, params LifecyclesParams) error {
|
||||
for _, entry := range registry.GetEntryRegistry() {
|
||||
for _, protocol := range params.Protocols {
|
||||
if protocol.Name() == entry.Key {
|
||||
lifecycle.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return protocol.Start(ctx)
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return protocol.Stop(ctx)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v2/model"
|
||||
"github.com/casbin/casbin/v2/persist"
|
||||
"go.uber.org/zap"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewCasbin(logger *zap.Logger) *casbin.Enforcer {
|
||||
m := model.NewModel()
|
||||
m.AddDef("r", "r", "sub, obj, act")
|
||||
m.AddDef("p", "p", "sub, obj, act")
|
||||
m.AddDef("g", "g", "_, _")
|
||||
m.AddDef("e", "e", "some(where (p.eft == allow))")
|
||||
m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act")
|
||||
|
||||
a := NewPolicyAdapter(logger)
|
||||
|
||||
e, err := casbin.NewEnforcer(m, a)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create casbin enforcer", zap.Error(err))
|
||||
}
|
||||
|
||||
// Add policies after creating the enforcer
|
||||
_ = a.AddPolicy("p", "p", []string{"admin", "/admin*"})
|
||||
|
||||
err = e.LoadPolicy()
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to load policies into Casbin model", zap.Error(err))
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
type PolicyAdapter struct {
|
||||
policy []string
|
||||
lock sync.RWMutex
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewPolicyAdapter creates a new PolicyAdapter instance.
|
||||
func NewPolicyAdapter(logger *zap.Logger) *PolicyAdapter {
|
||||
return &PolicyAdapter{
|
||||
policy: make([]string, 0),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPolicy loads all policy rules from the storage.
|
||||
func (a *PolicyAdapter) LoadPolicy(model model.Model) error {
|
||||
a.lock.RLock()
|
||||
defer a.lock.RUnlock()
|
||||
|
||||
for _, line := range a.policy {
|
||||
err := persist.LoadPolicyLine(line, model)
|
||||
if err != nil {
|
||||
a.logger.Fatal("Failed to load policy line", zap.Error(err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePolicy saves all policy rules to the storage.
|
||||
func (a *PolicyAdapter) SavePolicy(model model.Model) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPolicy adds a policy rule to the storage.
|
||||
// AddPolicy adds a policy rule to the storage.
|
||||
func (a *PolicyAdapter) AddPolicy(sec string, ptype string, rule []string) error {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
// Create a line representing the policy rule with the section
|
||||
line := sec + ", " + ptype + ", " + strings.Join(rule, ", ")
|
||||
|
||||
// Check if the policy rule already exists
|
||||
for _, existingLine := range a.policy {
|
||||
if line == existingLine {
|
||||
return nil // Policy rule already exists, no need to add it again
|
||||
}
|
||||
}
|
||||
|
||||
// Add the policy rule to the storage
|
||||
a.policy = append(a.policy, line)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePolicy removes a policy rule from the storage.
|
||||
func (a *PolicyAdapter) RemovePolicy(sec string, ptype string, rule []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
|
||||
func (a *PolicyAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/account"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"go.sia.tech/jape"
|
||||
)
|
||||
|
||||
const DEFAULT_AUTH_CONTEXT_KEY = "user_id"
|
||||
const AUTH_TOKEN_CONTEXT_KEY = "auth_token"
|
||||
|
||||
type JapeMiddlewareFunc func(jape.Handler) jape.Handler
|
||||
type HttpMiddlewareFunc func(http.Handler) http.Handler
|
||||
|
||||
type FindAuthTokenFunc func(r *http.Request) string
|
||||
|
||||
func AdaptMiddleware(mid func(http.Handler) http.Handler) JapeMiddlewareFunc {
|
||||
return jape.Adapt(func(h http.Handler) http.Handler {
|
||||
handler := mid(h)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ProxyMiddleware creates a new HTTP middleware for handling X-Forwarded-For headers.
|
||||
func ProxyMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
ips := strings.Split(xff, ", ")
|
||||
if len(ips) > 0 {
|
||||
r.RemoteAddr = ips[0]
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func ApplyMiddlewares(handler jape.Handler, middlewares ...interface{}) jape.Handler {
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
switch middlewares[i].(type) {
|
||||
case JapeMiddlewareFunc:
|
||||
mid := middlewares[i].(JapeMiddlewareFunc)
|
||||
handler = mid(handler)
|
||||
case func(http.Handler) http.Handler:
|
||||
mid := middlewares[i].(func(http.Handler) http.Handler)
|
||||
handler = AdaptMiddleware(mid)(handler)
|
||||
case HttpMiddlewareFunc:
|
||||
mid := middlewares[i].(HttpMiddlewareFunc)
|
||||
handler = AdaptMiddleware(mid)(handler)
|
||||
|
||||
default:
|
||||
panic("Invalid middleware type")
|
||||
}
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
func FindAuthToken(r *http.Request, cookieName string, queryParam string) string {
|
||||
authHeader := ParseAuthTokenHeader(r.Header)
|
||||
|
||||
if authHeader != "" {
|
||||
return authHeader
|
||||
}
|
||||
|
||||
if cookie, err := r.Cookie(cookieName); cookie != nil && err == nil {
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
if cookie, err := r.Cookie(account.AUTH_COOKIE_NAME); cookie != nil && err == nil {
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
return r.FormValue(queryParam)
|
||||
}
|
||||
|
||||
func ParseAuthTokenHeader(headers http.Header) string {
|
||||
authHeader := headers.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
authHeader = strings.TrimPrefix(authHeader, "Bearer ")
|
||||
authHeader = strings.TrimPrefix(authHeader, "bearer ")
|
||||
|
||||
return authHeader
|
||||
}
|
||||
|
||||
type AuthMiddlewareOptions struct {
|
||||
Identity ed25519.PrivateKey
|
||||
Accounts *account.AccountServiceDefault
|
||||
FindToken FindAuthTokenFunc
|
||||
Purpose account.JWTPurpose
|
||||
AuthContextKey string
|
||||
Config *config.Manager
|
||||
EmptyAllowed bool
|
||||
ExpiredAllowed bool
|
||||
}
|
||||
|
||||
func AuthMiddleware(options AuthMiddlewareOptions) func(http.Handler) http.Handler {
|
||||
if options.AuthContextKey == "" {
|
||||
options.AuthContextKey = DEFAULT_AUTH_CONTEXT_KEY
|
||||
}
|
||||
|
||||
domain := options.Config.Config().Core.Domain
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authToken := options.FindToken(r)
|
||||
|
||||
if authToken == "" {
|
||||
if !options.EmptyAllowed {
|
||||
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var audList *jwt.ClaimStrings
|
||||
|
||||
claim, err := account.JWTVerifyToken(authToken, domain, options.Identity, func(claim *jwt.RegisteredClaims) error {
|
||||
aud, _ := claim.GetAudience()
|
||||
|
||||
audList = &aud
|
||||
|
||||
if options.Purpose != account.JWTPurposeNone && jwtPurposeEqual(aud, options.Purpose) == false {
|
||||
return account.ErrJWTInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
unauthorized := true
|
||||
if errors.Is(err, jwt.ErrTokenExpired) && options.ExpiredAllowed {
|
||||
unauthorized = false
|
||||
}
|
||||
|
||||
if !unauthorized && audList == nil {
|
||||
if audList == nil {
|
||||
var claim jwt.RegisteredClaims
|
||||
|
||||
unverified, _, err := jwt.NewParser().ParseUnverified(authToken, &claim)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
audList, err := unverified.Claims.GetAudience()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if jwtPurposeEqual(audList, options.Purpose) == true {
|
||||
unauthorized = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if unauthorized {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if claim == nil && options.ExpiredAllowed {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := strconv.ParseUint(claim.Subject, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, account.ErrJWTInvalid.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
exists, _, err := options.Accounts.AccountExists(uint(userId))
|
||||
|
||||
if !exists || err != nil {
|
||||
http.Error(w, account.ErrJWTInvalid.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), options.AuthContextKey, uint(userId))
|
||||
ctx = context.WithValue(ctx, AUTH_TOKEN_CONTEXT_KEY, authToken)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func MergeRoutes(routes ...map[string]jape.Handler) map[string]jape.Handler {
|
||||
merged := make(map[string]jape.Handler)
|
||||
|
||||
for _, route := range routes {
|
||||
for k, v := range route {
|
||||
merged[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func GetUserFromContext(ctx context.Context, key ...string) uint {
|
||||
realKey := ""
|
||||
|
||||
if len(key) > 0 {
|
||||
realKey = key[0]
|
||||
}
|
||||
|
||||
if realKey == "" {
|
||||
realKey = DEFAULT_AUTH_CONTEXT_KEY
|
||||
}
|
||||
|
||||
userId, ok := ctx.Value(realKey).(uint)
|
||||
|
||||
if !ok {
|
||||
panic("user id stored in context is not of type uint")
|
||||
}
|
||||
|
||||
return userId
|
||||
}
|
||||
|
||||
func GetAuthTokenFromContext(ctx context.Context) string {
|
||||
authToken, ok := ctx.Value(AUTH_TOKEN_CONTEXT_KEY).(string)
|
||||
|
||||
if !ok {
|
||||
panic("auth token stored in context is not of type string")
|
||||
}
|
||||
|
||||
return authToken
|
||||
}
|
||||
|
||||
func CtxAborted(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func jwtPurposeEqual(aud jwt.ClaimStrings, purpose account.JWTPurpose) bool {
|
||||
return slices.Contains[jwt.ClaimStrings, string](aud, string(purpose))
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
router2 "git.lumeweb.com/LumeWeb/portal/api/router"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
Name() string
|
||||
Init() error
|
||||
Start(ctx context.Context) error
|
||||
Stop(ctx context.Context) error
|
||||
}
|
||||
|
||||
type APIEntry struct {
|
||||
Key string
|
||||
Module fx.Option
|
||||
}
|
||||
|
||||
var apiEntryRegistry []APIEntry
|
||||
var apiRegistry map[string]API
|
||||
var router *router2.APIRouter
|
||||
|
||||
func init() {
|
||||
router = router2.NewAPIRouter()
|
||||
apiRegistry = make(map[string]API)
|
||||
}
|
||||
|
||||
func RegisterEntry(entry APIEntry) {
|
||||
apiEntryRegistry = append(apiEntryRegistry, entry)
|
||||
}
|
||||
|
||||
func RegisterAPI(api API) {
|
||||
apiRegistry[api.Name()] = api
|
||||
}
|
||||
|
||||
func GetEntryRegistry() []APIEntry {
|
||||
return apiEntryRegistry
|
||||
}
|
||||
|
||||
func GetAPI(name string) API {
|
||||
if _, ok := apiRegistry[name]; !ok {
|
||||
panic("API not found: " + name)
|
||||
}
|
||||
|
||||
return apiRegistry[name]
|
||||
}
|
||||
|
||||
func GetAllAPIs() map[string]API {
|
||||
return apiRegistry
|
||||
}
|
||||
|
||||
func GetRouter() *router2.APIRouter {
|
||||
return router
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
type RoutableAPI interface {
|
||||
Name() string
|
||||
Domain() string
|
||||
AuthTokenName() string
|
||||
Can(w http.ResponseWriter, r *http.Request) bool
|
||||
Handle(w http.ResponseWriter, r *http.Request)
|
||||
Routes() (*httprouter.Router, error)
|
||||
}
|
||||
|
||||
type APIRouter struct {
|
||||
apis map[string]RoutableAPI
|
||||
apiDomain map[string]string
|
||||
apiHandlers map[string]http.Handler
|
||||
logger *zap.Logger
|
||||
config *config.Manager
|
||||
mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// Implement the ServeHTTP method on our new type
|
||||
func (hs APIRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if handler := hs.getHandlerByDomain(r.Host); handler != nil {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
for _, api := range hs.apis {
|
||||
if api.Can(w, r) {
|
||||
api.Handle(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
func (hs *APIRouter) RegisterAPI(impl RoutableAPI) {
|
||||
name := impl.Name()
|
||||
hs.apis[name] = impl
|
||||
hs.apiDomain[name+"."+hs.config.Config().Core.Domain] = name
|
||||
}
|
||||
|
||||
func (hs *APIRouter) getHandlerByDomain(domain string) http.Handler {
|
||||
if apiName := hs.apiDomain[domain]; apiName != "" {
|
||||
return hs.getHandler(apiName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *APIRouter) getHandler(protocol string) http.Handler {
|
||||
hs.mutex.RLock()
|
||||
handler, ok := hs.apiHandlers[protocol]
|
||||
hs.mutex.RUnlock()
|
||||
|
||||
if ok {
|
||||
return handler
|
||||
}
|
||||
|
||||
hs.mutex.Lock()
|
||||
defer hs.mutex.Unlock()
|
||||
|
||||
// Double-check if the handler was created while acquiring the write lock
|
||||
if handler, ok := hs.apiHandlers[protocol]; ok {
|
||||
return handler
|
||||
}
|
||||
|
||||
proto, ok := hs.apis[protocol]
|
||||
if !ok {
|
||||
hs.logger.Fatal("Protocol not found", zap.String("protocol", protocol))
|
||||
return nil
|
||||
}
|
||||
|
||||
routes, err := proto.Routes()
|
||||
if err != nil {
|
||||
hs.logger.Fatal("Error getting routes", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
hs.apiHandlers[protocol] = routes
|
||||
return routes
|
||||
}
|
||||
|
||||
func NewAPIRouter() *APIRouter {
|
||||
return &APIRouter{
|
||||
apis: make(map[string]RoutableAPI),
|
||||
apiHandlers: make(map[string]http.Handler),
|
||||
apiDomain: make(map[string]string),
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *APIRouter) SetLogger(logger *zap.Logger) {
|
||||
hs.logger = logger
|
||||
}
|
||||
|
||||
func (hs *APIRouter) SetConfig(config *config.Manager) {
|
||||
hs.config = config
|
||||
}
|
||||
|
||||
func BuildSubdomain(api RoutableAPI, cfg *config.Manager) string {
|
||||
return api.Name() + "." + cfg.Config().Core.Domain
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
//go:build s5
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/api/registry"
|
||||
"git.lumeweb.com/LumeWeb/portal/api/s5"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.RegisterEntry(registry.APIEntry{
|
||||
Key: "s5",
|
||||
Module: s5.Module,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package s5
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// S5-specific error keys
|
||||
const (
|
||||
// File-related errors
|
||||
ErrKeyFileUploadFailed = "ErrFileUploadFailed"
|
||||
ErrKeyFileDownloadFailed = "ErrFileDownloadFailed"
|
||||
ErrKeyMetadataFetchFailed = "ErrMetadataFetchFailed"
|
||||
ErrKeyInvalidFileFormat = "ErrInvalidFileFormat"
|
||||
ErrKeyUnsupportedFileType = "ErrUnsupportedFileType"
|
||||
ErrKeyFileProcessingFailed = "ErrFileProcessingFailed"
|
||||
|
||||
// Storage and data handling errors
|
||||
ErrKeyStorageOperationFailed = "ErrStorageOperationFailed"
|
||||
ErrKeyResourceNotFound = "ErrResourceNotFound"
|
||||
ErrKeyResourceLimitExceeded = "ErrResourceLimitExceeded"
|
||||
ErrKeyDataIntegrityError = "ErrDataIntegrityError"
|
||||
|
||||
// User and permission errors
|
||||
ErrKeyPermissionDenied = "ErrPermissionDenied"
|
||||
ErrKeyInvalidOperation = "ErrInvalidOperation"
|
||||
ErrKeyAuthenticationFailed = "ErrAuthenticationFailed"
|
||||
ErrKeyAuthorizationFailed = "ErrAuthorizationFailed"
|
||||
|
||||
// Network and communication errors
|
||||
ErrKeyNetworkError = "ErrNetworkError"
|
||||
ErrKeyServiceUnavailable = "ErrServiceUnavailable"
|
||||
|
||||
// General errors
|
||||
ErrKeyInternalError = "ErrInternalError"
|
||||
ErrKeyConfigurationError = "ErrConfigurationError"
|
||||
ErrKeyOperationTimeout = "ErrOperationTimeout"
|
||||
)
|
||||
|
||||
// Default error messages for S5-specific errors
|
||||
var defaultErrorMessages = map[string]string{
|
||||
ErrKeyFileUploadFailed: "File upload failed due to an internal error.",
|
||||
ErrKeyFileDownloadFailed: "File download failed.",
|
||||
ErrKeyMetadataFetchFailed: "Failed to fetch metadata for the resource.",
|
||||
ErrKeyInvalidFileFormat: "Invalid file format provided.",
|
||||
ErrKeyUnsupportedFileType: "Unsupported file type.",
|
||||
ErrKeyFileProcessingFailed: "Failed to process the file.",
|
||||
ErrKeyStorageOperationFailed: "Storage operation failed unexpectedly.",
|
||||
ErrKeyResourceNotFound: "The specified resource was not found.",
|
||||
ErrKeyResourceLimitExceeded: "The operation exceeded the resource limit.",
|
||||
ErrKeyDataIntegrityError: "Data integrity check failed.",
|
||||
ErrKeyPermissionDenied: "Permission denied for the requested operation.",
|
||||
ErrKeyInvalidOperation: "Invalid or unsupported operation requested.",
|
||||
ErrKeyAuthenticationFailed: "Authentication failed.",
|
||||
ErrKeyAuthorizationFailed: "Authorization failed or insufficient permissions.",
|
||||
ErrKeyNetworkError: "Network error or connectivity issue.",
|
||||
ErrKeyServiceUnavailable: "The requested service is temporarily unavailable.",
|
||||
ErrKeyInternalError: "An internal server error occurred.",
|
||||
ErrKeyConfigurationError: "Configuration error or misconfiguration detected.",
|
||||
ErrKeyOperationTimeout: "The operation timed out.",
|
||||
}
|
||||
|
||||
// Mapping of S5-specific error keys to HTTP status codes
|
||||
var errorCodeToHttpStatus = map[string]int{
|
||||
ErrKeyFileUploadFailed: http.StatusInternalServerError,
|
||||
ErrKeyFileDownloadFailed: http.StatusInternalServerError,
|
||||
ErrKeyMetadataFetchFailed: http.StatusInternalServerError,
|
||||
ErrKeyInvalidFileFormat: http.StatusBadRequest,
|
||||
ErrKeyUnsupportedFileType: http.StatusBadRequest,
|
||||
ErrKeyFileProcessingFailed: http.StatusInternalServerError,
|
||||
ErrKeyStorageOperationFailed: http.StatusInternalServerError,
|
||||
ErrKeyResourceNotFound: http.StatusNotFound,
|
||||
ErrKeyResourceLimitExceeded: http.StatusForbidden,
|
||||
ErrKeyDataIntegrityError: http.StatusInternalServerError,
|
||||
ErrKeyPermissionDenied: http.StatusForbidden,
|
||||
ErrKeyInvalidOperation: http.StatusBadRequest,
|
||||
ErrKeyAuthenticationFailed: http.StatusUnauthorized,
|
||||
ErrKeyAuthorizationFailed: http.StatusUnauthorized,
|
||||
ErrKeyNetworkError: http.StatusBadGateway,
|
||||
ErrKeyServiceUnavailable: http.StatusServiceUnavailable,
|
||||
ErrKeyInternalError: http.StatusInternalServerError,
|
||||
ErrKeyConfigurationError: http.StatusInternalServerError,
|
||||
ErrKeyOperationTimeout: http.StatusRequestTimeout,
|
||||
}
|
||||
|
||||
// S5Error struct for representing S5-specific errors
|
||||
type S5Error struct {
|
||||
Key string
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error method to implement the error interface
|
||||
func (e *S5Error) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *S5Error) HttpStatus() int {
|
||||
if code, exists := errorCodeToHttpStatus[e.Key]; exists {
|
||||
return code
|
||||
}
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
func NewS5Error(key string, err error, customMessage ...string) *S5Error {
|
||||
message, exists := defaultErrorMessages[key]
|
||||
if !exists {
|
||||
message = "An unknown error occurred"
|
||||
}
|
||||
if len(customMessage) > 0 {
|
||||
message = customMessage[0]
|
||||
}
|
||||
|
||||
return &S5Error{
|
||||
Key: key,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,547 @@
|
|||
package s5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
s5libmetadata "git.lumeweb.com/LumeWeb/libs5-go/metadata"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/protocols/s5"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/metadata"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/storage"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/encoding"
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/types"
|
||||
)
|
||||
|
||||
var _ io.ReadSeekCloser = (*S5File)(nil)
|
||||
var _ fs.File = (*S5File)(nil)
|
||||
var _ fs.ReadDirFile = (*S5File)(nil)
|
||||
var _ fs.DirEntry = (*S5File)(nil)
|
||||
var _ fs.FileInfo = (*S5FileInfo)(nil)
|
||||
|
||||
type S5File struct {
|
||||
reader io.ReadCloser
|
||||
hash []byte
|
||||
storage storage.StorageService
|
||||
metadata metadata.MetadataService
|
||||
record *metadata.UploadMetadata
|
||||
protocol *s5.S5Protocol
|
||||
cid *encoding.CID
|
||||
typ types.CIDType
|
||||
read bool
|
||||
tus *s5.TusHandler
|
||||
ctx context.Context
|
||||
name string
|
||||
root []byte
|
||||
rootType types.CIDType
|
||||
rootCid *encoding.CID
|
||||
}
|
||||
|
||||
func (f *S5File) IsDir() bool {
|
||||
return f.typ == types.CIDTypeDirectory
|
||||
}
|
||||
|
||||
func (f *S5File) Type() fs.FileMode {
|
||||
if f.typ == types.CIDTypeDirectory {
|
||||
return fs.ModeDir
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *S5File) Info() (fs.FileInfo, error) {
|
||||
return f.Stat()
|
||||
}
|
||||
|
||||
type FileParams struct {
|
||||
Storage storage.StorageService
|
||||
Metadata metadata.MetadataService
|
||||
Hash []byte
|
||||
Type types.CIDType
|
||||
Protocol *s5.S5Protocol
|
||||
Tus *s5.TusHandler
|
||||
Name string
|
||||
Root []byte
|
||||
RootType types.CIDType
|
||||
}
|
||||
|
||||
func NewFile(params FileParams) *S5File {
|
||||
return &S5File{
|
||||
storage: params.Storage,
|
||||
metadata: params.Metadata,
|
||||
hash: params.Hash,
|
||||
typ: params.Type,
|
||||
protocol: params.Protocol,
|
||||
tus: params.Tus,
|
||||
ctx: context.Background(),
|
||||
name: params.Name,
|
||||
root: params.Root,
|
||||
rootType: params.RootType,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *S5File) Exists() bool {
|
||||
ctx := context.Background()
|
||||
exists, _ := f.tus.UploadExists(ctx, f.hash)
|
||||
|
||||
if exists {
|
||||
return true
|
||||
}
|
||||
|
||||
_, err := f.metadata.GetUpload(context.Background(), f.hash)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *S5File) Read(p []byte) (n int, err error) {
|
||||
err = f.init(0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.read = true
|
||||
|
||||
return f.reader.Read(p)
|
||||
}
|
||||
|
||||
func (f *S5File) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
if !f.read && offset == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if f.reader != nil {
|
||||
err := f.reader.Close()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.reader = nil
|
||||
}
|
||||
err := f.init(offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
case io.SeekCurrent:
|
||||
return 0, errors.New("not supported")
|
||||
case io.SeekEnd:
|
||||
return int64(f.Size()), nil
|
||||
default:
|
||||
return 0, errors.New("invalid whence")
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (f *S5File) Close() error {
|
||||
if f.reader != nil {
|
||||
r := f.reader
|
||||
f.reader = nil
|
||||
return r.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *S5File) init(offset int64) error {
|
||||
if f.reader == nil {
|
||||
reader, err := f.tus.GetUploadReader(f.ctx, f.hash, offset)
|
||||
|
||||
if err == nil {
|
||||
f.reader = reader
|
||||
f.read = false
|
||||
return nil
|
||||
}
|
||||
|
||||
reader, err = f.storage.DownloadObject(context.Background(), f.StorageProtocol(), f.hash, offset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.reader = reader
|
||||
f.read = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *S5File) Record() (*metadata.UploadMetadata, error) {
|
||||
if f.record == nil {
|
||||
exists, tusRecord := f.tus.UploadExists(context.Background(), f.hash)
|
||||
|
||||
if exists {
|
||||
size, err := f.tus.GetUploadSize(context.Background(), f.hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &metadata.UploadMetadata{
|
||||
Hash: f.hash,
|
||||
Size: uint64(size),
|
||||
MimeType: tusRecord.MimeType,
|
||||
Created: tusRecord.CreatedAt,
|
||||
Protocol: f.protocol.Name(),
|
||||
UploaderIP: tusRecord.UploaderIP,
|
||||
UserID: tusRecord.UploaderID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
record, err := f.metadata.GetUpload(context.Background(), f.hash)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New("file does not exist")
|
||||
}
|
||||
|
||||
f.record = &record
|
||||
}
|
||||
|
||||
return f.record, nil
|
||||
}
|
||||
|
||||
func (f *S5File) Hash() []byte {
|
||||
hashStr := f.HashString()
|
||||
|
||||
if hashStr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
str, err := hex.DecodeString(hashStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (f *S5File) HashString() string {
|
||||
record, err := f.Record()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return hex.EncodeToString(record.Hash)
|
||||
}
|
||||
|
||||
func (f *S5File) Name() string {
|
||||
if f.name != "" {
|
||||
return f.name
|
||||
}
|
||||
|
||||
cid, _ := f.CID().ToString()
|
||||
|
||||
return cid
|
||||
}
|
||||
|
||||
func (f *S5File) Modtime() time.Time {
|
||||
record, err := f.Record()
|
||||
if err != nil {
|
||||
return time.Unix(0, 0)
|
||||
}
|
||||
|
||||
return record.Created
|
||||
}
|
||||
func (f *S5File) Size() uint64 {
|
||||
record, err := f.Record()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return record.Size
|
||||
}
|
||||
func (f *S5File) CID() *encoding.CID {
|
||||
if f.cid == nil {
|
||||
multihash := encoding.MultihashFromBytes(f.Hash(), types.HashTypeBlake3)
|
||||
|
||||
typ := f.typ
|
||||
if typ == 0 {
|
||||
typ = types.CIDTypeRaw
|
||||
}
|
||||
|
||||
cid := encoding.NewCID(typ, *multihash, f.Size())
|
||||
f.cid = cid
|
||||
}
|
||||
return f.cid
|
||||
}
|
||||
|
||||
func (f *S5File) RootCID() *encoding.CID {
|
||||
if f.rootCid == nil {
|
||||
if f.root == nil {
|
||||
return nil
|
||||
}
|
||||
multihash := encoding.MultihashFromBytes(f.root, types.HashTypeBlake3)
|
||||
typ := f.rootType
|
||||
if typ == 0 {
|
||||
typ = types.CIDTypeRaw
|
||||
}
|
||||
|
||||
cid := encoding.NewCID(typ, *multihash, f.Size())
|
||||
f.rootCid = cid
|
||||
|
||||
}
|
||||
|
||||
return f.rootCid
|
||||
}
|
||||
|
||||
func (f *S5File) Mime() string {
|
||||
record, err := f.Record()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return record.MimeType
|
||||
}
|
||||
|
||||
func (f *S5File) StorageProtocol() storage.StorageProtocol {
|
||||
return s5.GetStorageProtocol(f.protocol)
|
||||
}
|
||||
|
||||
func (f *S5File) Proof() ([]byte, error) {
|
||||
object, err := f.storage.DownloadObjectProof(context.Background(), f.StorageProtocol(), f.hash)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proof, err := io.ReadAll(object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = object.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return proof, nil
|
||||
}
|
||||
func (f *S5File) Manifest() (s5libmetadata.Metadata, error) {
|
||||
cid := f.RootCID()
|
||||
|
||||
if cid == nil {
|
||||
cid = f.CID()
|
||||
}
|
||||
|
||||
if f.Exists() {
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = f.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md, err := f.protocol.Node().Services().Storage().ParseMetadata(data, cid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return md, nil
|
||||
}
|
||||
|
||||
meta, err := f.protocol.Node().Services().Storage().GetMetadataByCID(cid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (f *S5File) Stat() (fs.FileInfo, error) {
|
||||
return newS5FileInfo(f), nil
|
||||
}
|
||||
|
||||
type S5FileInfo struct {
|
||||
file *S5File
|
||||
}
|
||||
|
||||
func (s S5FileInfo) Name() string {
|
||||
return s.file.Name()
|
||||
}
|
||||
|
||||
func (s S5FileInfo) Size() int64 {
|
||||
return int64(s.file.Size())
|
||||
}
|
||||
|
||||
func (s S5FileInfo) Mode() fs.FileMode {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s S5FileInfo) ModTime() time.Time {
|
||||
return s.file.Modtime()
|
||||
}
|
||||
|
||||
func (s S5FileInfo) IsDir() bool {
|
||||
if s.file.name == "." {
|
||||
return true
|
||||
}
|
||||
|
||||
manifest, err := s.file.Manifest()
|
||||
if err == nil && s.file.root != nil {
|
||||
webApp, ok := manifest.(*s5libmetadata.WebAppMetadata)
|
||||
if ok {
|
||||
if slices.Contains(webApp.TryFiles, path.Base(s.file.name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.file.typ == types.CIDTypeDirectory
|
||||
}
|
||||
|
||||
func (s S5FileInfo) Sys() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *S5File) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
manifest, err := f.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch f.CID().Type {
|
||||
case types.CIDTypeDirectory:
|
||||
dir, ok := manifest.(*s5libmetadata.DirectoryMetadata)
|
||||
if !ok {
|
||||
return nil, errors.New("manifest is not a directory")
|
||||
}
|
||||
|
||||
var entries []fs.DirEntry
|
||||
|
||||
for _, file := range dir.Files.Items() {
|
||||
entries = append(entries, NewFile(FileParams{
|
||||
Storage: f.storage,
|
||||
Metadata: f.metadata,
|
||||
Hash: file.File.CID().Hash.HashBytes(),
|
||||
Type: file.File.CID().Type,
|
||||
Tus: f.tus,
|
||||
Name: file.Name,
|
||||
}))
|
||||
}
|
||||
|
||||
for _, subDir := range dir.Directories.Items() {
|
||||
cid, err := resolveDirCid(subDir, f.protocol.Node())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, NewFile(FileParams{
|
||||
Storage: f.storage,
|
||||
Metadata: f.metadata,
|
||||
Hash: cid.Hash.HashBytes(),
|
||||
Type: cid.Type,
|
||||
Name: subDir.Name,
|
||||
}))
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
|
||||
case types.CIDTypeMetadataWebapp:
|
||||
webApp, ok := manifest.(*s5libmetadata.WebAppMetadata)
|
||||
if !ok {
|
||||
return nil, errors.New("manifest is not a web app")
|
||||
}
|
||||
|
||||
var entries []fs.DirEntry
|
||||
dirMap := make(map[string]bool)
|
||||
|
||||
webApp.Paths.Keys()
|
||||
|
||||
for _, path := range webApp.Paths.Keys() {
|
||||
pathSegments := strings.Split(path, "/")
|
||||
|
||||
// Check if the path is an immediate child (either a file or a direct subdirectory)
|
||||
if len(pathSegments) == 1 {
|
||||
// It's a file directly within `dirPath`
|
||||
entries = append(entries, newWebAppEntry(pathSegments[0], false))
|
||||
} else if len(pathSegments) > 1 {
|
||||
// It's a subdirectory, but ensure to add each unique subdirectory only once
|
||||
subDirName := pathSegments[0] // The immediate subdirectory name
|
||||
if _, exists := dirMap[subDirName]; !exists {
|
||||
entries = append(entries, newWebAppEntry(subDirName, true))
|
||||
dirMap[subDirName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].Name() < entries[j].Name()
|
||||
})
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported CID type")
|
||||
}
|
||||
|
||||
func newS5FileInfo(file *S5File) *S5FileInfo {
|
||||
return &S5FileInfo{
|
||||
file: file,
|
||||
}
|
||||
}
|
||||
|
||||
type webAppEntry struct {
|
||||
name string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func newWebAppEntry(name string, isDir bool) *webAppEntry {
|
||||
return &webAppEntry{name: name, isDir: isDir}
|
||||
}
|
||||
|
||||
func (d *webAppEntry) Name() string {
|
||||
return d.name
|
||||
}
|
||||
|
||||
func (d *webAppEntry) IsDir() bool {
|
||||
return d.isDir
|
||||
}
|
||||
|
||||
func (d *webAppEntry) Type() fs.FileMode {
|
||||
if d.isDir {
|
||||
return fs.ModeDir
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d *webAppEntry) Info() (fs.FileInfo, error) {
|
||||
return &webAppFileInfo{name: d.name, isDir: true}, nil
|
||||
}
|
||||
|
||||
type webAppFileInfo struct {
|
||||
name string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (fi *webAppFileInfo) Name() string { return fi.name }
|
||||
func (fi *webAppFileInfo) Size() int64 { return 0 }
|
||||
func (fi *webAppFileInfo) Mode() fs.FileMode {
|
||||
if fi.isDir {
|
||||
return fs.ModeDir
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
func (fi *webAppFileInfo) ModTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
func (fi *webAppFileInfo) IsDir() bool {
|
||||
return fi.isDir
|
||||
}
|
||||
func (fi *webAppFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package s5
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/node"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/encoding"
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/metadata"
|
||||
)
|
||||
|
||||
var _ fs.FS = (*dirFs)(nil)
|
||||
|
||||
type dirFs struct {
|
||||
root *encoding.CID
|
||||
s5 *S5API
|
||||
}
|
||||
|
||||
func (w *dirFs) Open(name string) (fs.File, error) {
|
||||
file := w.s5.newFile(FileParams{
|
||||
Hash: w.root.Hash.HashBytes(),
|
||||
Type: w.root.Type,
|
||||
})
|
||||
|
||||
manifest, err := file.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dir, ok := manifest.(*metadata.DirectoryMetadata)
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("manifest is not a directory")
|
||||
}
|
||||
|
||||
segments := strings.Split(name, "/")
|
||||
|
||||
if len(segments) == 1 {
|
||||
return w.openDirectly(name, dir)
|
||||
}
|
||||
|
||||
nextDirName := segments[0]
|
||||
remainingPath := strings.Join(segments[1:], "/")
|
||||
|
||||
return w.openNestedDir(nextDirName, remainingPath, dir)
|
||||
}
|
||||
|
||||
func (w *dirFs) openDirectly(name string, dir *metadata.DirectoryMetadata) (fs.File, error) {
|
||||
file := dir.Files.Get(name)
|
||||
subDir := dir.Directories.Get(name)
|
||||
|
||||
if file != nil {
|
||||
return w.s5.newFile(FileParams{
|
||||
Hash: file.File.CID().Hash.HashBytes(),
|
||||
Type: file.File.CID().Type,
|
||||
Name: file.Name,
|
||||
}), nil
|
||||
}
|
||||
|
||||
if subDir != nil {
|
||||
cid, err := w.resolveDirCid(subDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w.s5.newFile(FileParams{
|
||||
Hash: cid.Hash.HashBytes(),
|
||||
Type: cid.Type,
|
||||
Name: name,
|
||||
}), nil
|
||||
}
|
||||
|
||||
if name == "." {
|
||||
return w.s5.newFile(FileParams{
|
||||
Hash: w.root.Hash.HashBytes(),
|
||||
Type: w.root.Type,
|
||||
Name: name,
|
||||
}), nil
|
||||
|
||||
}
|
||||
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
func (w dirFs) openNestedDir(name string, remainingPath string, dir *metadata.DirectoryMetadata) (fs.File, error) {
|
||||
subDir := dir.Directories.Get(name)
|
||||
|
||||
if subDir == nil {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
cid, err := w.resolveDirCid(subDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nestedFs := newDirFs(cid, w.s5)
|
||||
|
||||
return nestedFs.Open(remainingPath)
|
||||
|
||||
}
|
||||
|
||||
func (w *dirFs) resolveDirCid(dir *metadata.DirectoryReference) (*encoding.CID, error) {
|
||||
return resolveDirCid(dir, w.s5.getNode())
|
||||
}
|
||||
|
||||
func newDirFs(root *encoding.CID, s5 *S5API) *dirFs {
|
||||
return &dirFs{
|
||||
root: root,
|
||||
s5: s5,
|
||||
}
|
||||
}
|
||||
func resolveDirCid(dir *metadata.DirectoryReference, node *node.Node) (*encoding.CID, error) {
|
||||
if len(dir.PublicKey) == 0 {
|
||||
return nil, errors.New("missing public key")
|
||||
}
|
||||
|
||||
entry, err := node.Services().Registry().Get(dir.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cid, err := encoding.CIDFromRegistry(entry.Data())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cid, nil
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package s5
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/encoding"
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/metadata"
|
||||
)
|
||||
|
||||
var _ fs.FS = (*webAppFs)(nil)
|
||||
|
||||
type webAppFs struct {
|
||||
root *encoding.CID
|
||||
s5 *S5API
|
||||
}
|
||||
|
||||
func (w webAppFs) Open(name string) (fs.File, error) {
|
||||
file := w.s5.newFile(FileParams{
|
||||
Hash: w.root.Hash.HashBytes(),
|
||||
Type: w.root.Type,
|
||||
})
|
||||
|
||||
manifest, err := file.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webApp, ok := manifest.(*metadata.WebAppMetadata)
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("manifest is not a web app")
|
||||
}
|
||||
|
||||
if name == "." {
|
||||
return w.s5.newFile(FileParams{
|
||||
Hash: w.root.Hash.HashBytes(),
|
||||
Type: w.root.Type,
|
||||
Name: name,
|
||||
}), nil
|
||||
}
|
||||
|
||||
item, ok := webApp.Paths.Get(name)
|
||||
|
||||
if !ok {
|
||||
name = path.Join(name, "index.html")
|
||||
item, ok = webApp.Paths.Get(name)
|
||||
if !ok {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
return w.s5.newFile(FileParams{
|
||||
Hash: item.Cid.Hash.HashBytes(),
|
||||
Type: item.Cid.Type,
|
||||
Name: name,
|
||||
Root: w.root.Hash.HashBytes(),
|
||||
RootType: w.root.Type,
|
||||
}), nil
|
||||
}
|
||||
return w.s5.newFile(FileParams{
|
||||
Hash: item.Cid.Hash.HashBytes(),
|
||||
Type: item.Cid.Type,
|
||||
Name: name,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func newWebAppFs(root *encoding.CID, s5 *S5API) *webAppFs {
|
||||
return &webAppFs{
|
||||
root: root,
|
||||
s5: s5,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package s5
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/encoding"
|
||||
"git.lumeweb.com/LumeWeb/libs5-go/types"
|
||||
"git.lumeweb.com/LumeWeb/portal/db/models"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
_ msgpack.CustomEncoder = (*AccountPinBinaryResponse)(nil)
|
||||
)
|
||||
|
||||
type AccountRegisterRequest struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
Response string `json:"response"`
|
||||
Signature string `json:"signature"`
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
type SmallUploadResponse struct {
|
||||
CID string `json:"cid"`
|
||||
}
|
||||
type AccountRegisterChallengeResponse struct {
|
||||
Challenge string `json:"challenge"`
|
||||
}
|
||||
|
||||
type AccountLoginRequest struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
Response string `json:"response"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
type AccountLoginChallengeResponse struct {
|
||||
Challenge string `json:"challenge"`
|
||||
}
|
||||
type AccountInfoResponse struct {
|
||||
Email string `json:"email"`
|
||||
QuotaExceeded bool `json:"quotaExceeded"`
|
||||
EmailConfirmed bool `json:"emailConfirmed"`
|
||||
IsRestricted bool `json:"isRestricted"`
|
||||
Tier AccountTier `json:"tier"`
|
||||
}
|
||||
|
||||
type AccountStatsResponse struct {
|
||||
AccountInfoResponse
|
||||
Stats AccountStats `json:"stats"`
|
||||
}
|
||||
|
||||
type AccountTier struct {
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UploadBandwidth uint64 `json:"uploadBandwidth"`
|
||||
StorageLimit uint64 `json:"storageLimit"`
|
||||
Scopes []interface{} `json:"scopes"`
|
||||
}
|
||||
|
||||
type AccountStats struct {
|
||||
Total AccountStatsTotal `json:"total"`
|
||||
}
|
||||
|
||||
type AccountStatsTotal struct {
|
||||
UsedStorage uint64 `json:"usedStorage"`
|
||||
}
|
||||
type AppUploadResponse struct {
|
||||
CID string `json:"cid"`
|
||||
}
|
||||
type RegistryQueryResponse struct {
|
||||
Pk string `json:"pk"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Data string `json:"data"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type RegistrySetRequest struct {
|
||||
Pk string `json:"pk"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Data string `json:"data"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type DebugStorageLocation struct {
|
||||
Type int `json:"type"`
|
||||
Parts []string `json:"parts"`
|
||||
Expiry int64 `json:"expiry"`
|
||||
NodeId string `json:"nodeId"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
type DebugStorageLocationsResponse struct {
|
||||
Locations []DebugStorageLocation `json:"locations"`
|
||||
}
|
||||
|
||||
type AccountPinBinaryResponse struct {
|
||||
Pins []models.Pin
|
||||
Cursor uint64
|
||||
}
|
||||
|
||||
func (a AccountPinBinaryResponse) EncodeMsgpack(enc *msgpack.Encoder) error {
|
||||
err := enc.EncodeInt(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = enc.EncodeInt(int64(a.Cursor))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = enc.EncodeArrayLen(len(a.Pins))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pin := range a.Pins {
|
||||
err = enc.EncodeBytes(encoding.MultihashFromBytes(pin.Upload.Hash, types.HashTypeBlake3).FullBytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AccountPinResponse struct {
|
||||
Pins []AccountPin `json:"pins"`
|
||||
}
|
||||
|
||||
type AccountPin struct {
|
||||
Hash string `json:"hash"`
|
||||
Size uint64 `json:"size"`
|
||||
PinnedAt time.Time `json:"pinned_at"`
|
||||
MimeType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
type AccountPinStatusResponse struct {
|
||||
Status models.ImportStatus `json:"status"`
|
||||
Progress float64 `json:"progress"`
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package s5
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/api/middleware"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
authCookieName = "s5-auth-token"
|
||||
authQueryParam = "auth_token"
|
||||
)
|
||||
|
||||
func findToken(r *http.Request) string {
|
||||
return middleware.FindAuthToken(r, authCookieName, authQueryParam)
|
||||
}
|
||||
|
||||
func authMiddleware(options middleware.AuthMiddlewareOptions) middleware.HttpMiddlewareFunc {
|
||||
options.FindToken = findToken
|
||||
return middleware.AuthMiddleware(options)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,111 @@
|
|||
// This program downloads the dist assets for the current swagger-ui version and places them into the embed directory
|
||||
// TODO: Compress?
|
||||
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type releaseResp []struct {
|
||||
// TagName is a release tag name
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
releases := releaseResp{}
|
||||
// get the releases so we can download the latest one
|
||||
req, _ := http.NewRequest("GET", "https://api.github.com/repos/swagger-api/swagger-ui/releases", nil)
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Fatalf("error getting release list: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Fatalf("got status [%s] on release list download", resp.Status)
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
|
||||
log.Fatalf("error decoding response: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if len(releases) == 0 {
|
||||
log.Fatal("somehow got no releases, nothing to do")
|
||||
}
|
||||
tag := releases[0].TagName
|
||||
|
||||
log.Printf("downloading release %s...", tag)
|
||||
|
||||
resp, err = http.Get(fmt.Sprintf("https://github.com/swagger-api/swagger-ui/archive/%s.tar.gz", tag))
|
||||
if err != nil {
|
||||
log.Fatalf("error downloading release archive: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Fatalf("got status [%s] on release archive download", resp.Status)
|
||||
}
|
||||
zr, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening file as gzip: %v", err)
|
||||
}
|
||||
if err := os.RemoveAll("embed"); err != nil {
|
||||
log.Fatalf("error removing old embed directory")
|
||||
}
|
||||
if err := os.Mkdir("embed", 0o700); err != nil {
|
||||
log.Fatalf("error recreating embed directory")
|
||||
}
|
||||
tr := tar.NewReader(zr)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("tar parsing error: %v", err)
|
||||
}
|
||||
if header.Typeflag == tar.TypeReg {
|
||||
// got a file, remove version directory
|
||||
fname := header.Name[strings.Index(header.Name, `/`):]
|
||||
if strings.HasPrefix(fname, `/dist`) {
|
||||
fname = strings.TrimPrefix(fname, `/dist`)
|
||||
out, err := os.Create(filepath.Join("embed", fname))
|
||||
if err != nil {
|
||||
log.Fatalf("error create output file: %v", err)
|
||||
}
|
||||
if _, err := io.Copy(out, tr); err != nil {
|
||||
log.Fatalf("error writing output file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// replace the hard-coded JSON file with a generic file and disable the topbar
|
||||
initFile, err := os.ReadFile(filepath.Join("embed", "swagger-initializer.js"))
|
||||
if err != nil {
|
||||
log.Fatalf("error opening swagger-initializer.js for templating :%v", err)
|
||||
}
|
||||
newInit := regexp.MustCompile(`url:\s+"[^"]*"`).ReplaceAllLiteral(initFile, []byte(`url: "/swagger.json"`))
|
||||
newInit = regexp.MustCompile(`,?\s+SwaggerUIStandalonePreset.*\n`).ReplaceAllLiteral(newInit, []byte("\n"))
|
||||
newInit = regexp.MustCompile(`(?s),\s+plugins: \[.*],\n`).ReplaceAllLiteral(newInit, []byte("\n"))
|
||||
newInit = regexp.MustCompile(`\n\s*layout:.*\n`).ReplaceAllLiteral(newInit, []byte("\n"))
|
||||
newinitFile, err := os.Create(filepath.Join("embed", "swagger-initializer.js"))
|
||||
if err != nil {
|
||||
log.Fatalf("error re-creating swagger-initializer.js file: %v", err)
|
||||
}
|
||||
defer newinitFile.Close()
|
||||
if _, err := newinitFile.Write(newInit); err != nil {
|
||||
log.Fatalf("unable to write to swagger-initializer.js: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/api/middleware"
|
||||
|
||||
"go.sia.tech/jape"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
//go:generate go run generate.go
|
||||
|
||||
//go:embed embed
|
||||
var swagfs embed.FS
|
||||
|
||||
func byteHandler(b []byte) jape.Handler {
|
||||
return func(c jape.Context) {
|
||||
c.ResponseWriter.Header().Set("Content-Type", "application/json")
|
||||
c.ResponseWriter.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
func Swagger(spec []byte, routes map[string]jape.Handler) (map[string]jape.Handler, error) {
|
||||
loader := openapi3.NewLoader()
|
||||
doc, err := loader.LoadFromData(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = doc.Validate(loader.Context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonDoc, err := doc.MarshalJSON()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swaggerFiles, _ := fs.Sub(swagfs, "embed")
|
||||
swaggerServ := http.FileServer(http.FS(swaggerFiles))
|
||||
handler := func(c jape.Context) {
|
||||
swaggerServ.ServeHTTP(c.ResponseWriter, c.Request)
|
||||
}
|
||||
|
||||
strip := func(next http.Handler) http.Handler {
|
||||
return http.StripPrefix("/swagger", next)
|
||||
}
|
||||
|
||||
redirect := func(jc jape.Context) {
|
||||
http.Redirect(jc.ResponseWriter, jc.Request, "/swagger/", http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
swagRoutes := map[string]jape.Handler{
|
||||
"GET /swagger.json": byteHandler(jsonDoc),
|
||||
"GET /swagger": redirect,
|
||||
"GET /swagger/*path": middleware.ApplyMiddlewares(handler, strip),
|
||||
}
|
||||
|
||||
return middleware.MergeRoutes(routes, swagRoutes), nil
|
||||
}
|
154
bao/bao.go
154
bao/bao.go
|
@ -1,31 +1,161 @@
|
|||
package bao
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"io"
|
||||
"lukechampine.com/blake3"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"lukechampine.com/blake3/bao"
|
||||
)
|
||||
|
||||
func ComputeTree(reader io.Reader, size int64) ([]byte, [32]byte, error) {
|
||||
bufSize := blake3.BaoEncodedSize(int(size), true)
|
||||
buf := bufferAt{buf: make([]byte, bufSize)}
|
||||
var _ io.ReadCloser = (*Verifier)(nil)
|
||||
var _ io.WriterAt = (*proofWriter)(nil)
|
||||
|
||||
hash, err := blake3.BaoEncode(&buf, bufio.NewReader(reader), size, true)
|
||||
var ErrVerifyFailed = errors.New("verification failed")
|
||||
|
||||
const groupLog = 8
|
||||
const groupChunks = 1 << groupLog
|
||||
|
||||
type Verifier struct {
|
||||
r io.ReadCloser
|
||||
proof Result
|
||||
read uint64
|
||||
buffer *bytes.Buffer
|
||||
logger *zap.Logger
|
||||
readTime []time.Duration
|
||||
verifyTime time.Duration
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Hash []byte
|
||||
Proof []byte
|
||||
Length uint
|
||||
}
|
||||
|
||||
func (v *Verifier) Read(p []byte) (int, error) {
|
||||
// Initial attempt to read from the buffer
|
||||
n, err := v.buffer.Read(p)
|
||||
if n == len(p) {
|
||||
// If the buffer already had enough data to fulfill the request, return immediately
|
||||
return n, nil
|
||||
} else if err != nil && err != io.EOF {
|
||||
// For errors other than EOF, return the error immediately
|
||||
return n, err
|
||||
}
|
||||
|
||||
buf := make([]byte, groupChunks)
|
||||
// Continue reading from the source and verifying until we have enough data or hit an error
|
||||
for v.buffer.Len() < len(p)-n {
|
||||
readStart := time.Now()
|
||||
bytesRead, err := io.ReadFull(v.r, buf)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
return n, err // Return any read error immediately
|
||||
}
|
||||
|
||||
readEnd := time.Now()
|
||||
|
||||
v.readTime = append(v.readTime, readEnd.Sub(readStart))
|
||||
|
||||
timeStart := time.Now()
|
||||
|
||||
if bytesRead > 0 {
|
||||
if status := bao.VerifyChunk(buf[:bytesRead], v.proof.Proof, groupChunks, v.read, [32]byte(v.proof.Hash)); !status {
|
||||
return n, errors.Join(ErrVerifyFailed, err)
|
||||
}
|
||||
v.read += uint64(bytesRead)
|
||||
v.buffer.Write(buf[:bytesRead]) // Append new data to the buffer
|
||||
}
|
||||
|
||||
timeEnd := time.Now()
|
||||
v.verifyTime += timeEnd.Sub(timeStart)
|
||||
|
||||
if err == io.EOF {
|
||||
// If EOF, break the loop as no more data can be read
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.readTime) > 0 {
|
||||
averageReadTime := lo.Reduce(v.readTime, func(acc time.Duration, cur time.Duration, _ int) time.Duration {
|
||||
return acc + cur
|
||||
}, time.Duration(0)) / time.Duration(len(v.readTime))
|
||||
|
||||
v.logger.Debug("Read time", zap.Duration("average", averageReadTime))
|
||||
}
|
||||
|
||||
averageVerifyTime := v.verifyTime / time.Duration(v.read/groupChunks)
|
||||
v.logger.Debug("Verification time", zap.Duration("average", averageVerifyTime))
|
||||
|
||||
// Attempt to read the remainder of the data from the buffer
|
||||
additionalBytes, _ := v.buffer.Read(p[n:])
|
||||
return n + additionalBytes, nil
|
||||
}
|
||||
|
||||
func (v *Verifier) Close() error {
|
||||
return v.r.Close()
|
||||
}
|
||||
func Hash(r io.Reader, size uint64) (*Result, error) {
|
||||
reader := newSizeReader(r)
|
||||
writer := newProofWriter(int(size))
|
||||
|
||||
hash, err := bao.Encode(writer, reader, int64(size), groupLog, true)
|
||||
if err != nil {
|
||||
return nil, [32]byte{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.buf, hash, nil
|
||||
return &Result{
|
||||
Hash: hash[:],
|
||||
Proof: writer.buf,
|
||||
Length: uint(size),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type bufferAt struct {
|
||||
func NewVerifier(r io.ReadCloser, proof Result, logger *zap.Logger) *Verifier {
|
||||
return &Verifier{
|
||||
r: r,
|
||||
proof: proof,
|
||||
buffer: new(bytes.Buffer),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type proofWriter struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (b *bufferAt) WriteAt(p []byte, off int64) (int, error) {
|
||||
if copy(b.buf[off:], p) != len(p) {
|
||||
func (p proofWriter) WriteAt(b []byte, off int64) (n int, err error) {
|
||||
if copy(p.buf[off:], b) != len(b) {
|
||||
panic("bad buffer size")
|
||||
}
|
||||
return len(p), nil
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func newProofWriter(size int) *proofWriter {
|
||||
return &proofWriter{
|
||||
buf: make([]byte, bao.EncodedSize(size, groupLog, true)),
|
||||
}
|
||||
}
|
||||
|
||||
type sizeReader struct {
|
||||
reader io.Reader
|
||||
read int64
|
||||
}
|
||||
|
||||
func (s sizeReader) Read(p []byte) (int, error) {
|
||||
n, err := s.reader.Read(p)
|
||||
s.read += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func newSizeReader(r io.Reader) *sizeReader {
|
||||
return &sizeReader{
|
||||
reader: r,
|
||||
read: 0,
|
||||
}
|
||||
}
|
||||
|
|
100
cid/cid.go
100
cid/cid.go
|
@ -1,100 +0,0 @@
|
|||
package cid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"github.com/multiformats/go-multibase"
|
||||
)
|
||||
|
||||
var MAGIC_BYTES = []byte{0x26, 0x1f}
|
||||
|
||||
var (
|
||||
ErrMissingEmptySize = errors.New("Missing or empty size")
|
||||
ErrInvalidCIDMagic = errors.New("CID magic bytes missing or invalid")
|
||||
)
|
||||
|
||||
type CID struct {
|
||||
Hash [32]byte
|
||||
Size uint64
|
||||
}
|
||||
|
||||
func (c CID) StringHash() string {
|
||||
return hex.EncodeToString(c.Hash[:])
|
||||
}
|
||||
|
||||
func Encode(hash []byte, size uint64) (string, error) {
|
||||
var hashBytes [32]byte
|
||||
copy(hashBytes[:], hash)
|
||||
|
||||
return EncodeFixed(hashBytes, size)
|
||||
}
|
||||
|
||||
func EncodeFixed(hash [32]byte, size uint64) (string, error) {
|
||||
sizeBytes := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(sizeBytes, size)
|
||||
|
||||
prefixedHash := append(MAGIC_BYTES, hash[:]...)
|
||||
prefixedHash = append(prefixedHash, sizeBytes...)
|
||||
|
||||
return multibase.Encode(multibase.Base58BTC, prefixedHash)
|
||||
}
|
||||
|
||||
func EncodeString(hash string, size uint64) (string, error) {
|
||||
hashBytes, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return Encode(hashBytes, size)
|
||||
}
|
||||
|
||||
func Valid(cid string) (bool, error) {
|
||||
_, err := maybeDecode(cid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
}
|
||||
|
||||
func Decode(cid string) (*CID, error) {
|
||||
data, err := maybeDecode(cid)
|
||||
if err != nil {
|
||||
return &CID{}, err
|
||||
}
|
||||
|
||||
data = data[len(MAGIC_BYTES):]
|
||||
var hash [32]byte
|
||||
copy(hash[:], data[:])
|
||||
size := binary.LittleEndian.Uint64(data[32:])
|
||||
|
||||
return &CID{Hash: hash, Size: size}, nil
|
||||
}
|
||||
|
||||
func maybeDecode(cid string) ([]byte, error) {
|
||||
_, data, err := multibase.Decode(cid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Compare(data[0:len(MAGIC_BYTES)], MAGIC_BYTES) != 0 {
|
||||
return nil, ErrInvalidCIDMagic
|
||||
}
|
||||
|
||||
sizeBytes := data[len(MAGIC_BYTES)+32:]
|
||||
|
||||
if len(sizeBytes) == 0 {
|
||||
return nil, ErrMissingEmptySize
|
||||
}
|
||||
|
||||
size := binary.LittleEndian.Uint64(sizeBytes)
|
||||
|
||||
if size == 0 {
|
||||
return nil, ErrMissingEmptySize
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/api/router"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/api/registry"
|
||||
"go.sia.tech/core/wallet"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func NewIdentity(config *config.Manager, logger *zap.Logger) (ed25519.PrivateKey, error) {
|
||||
var seed [32]byte
|
||||
identitySeed := config.Config().Core.Identity
|
||||
|
||||
if identitySeed == "" {
|
||||
logger.Info("Generating new identity seed")
|
||||
identitySeed = wallet.NewSeedPhrase()
|
||||
config.Viper().Set("core.identity", identitySeed)
|
||||
err := config.Save()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err := wallet.SeedFromPhrase(&seed, identitySeed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ed25519.PrivateKey(wallet.KeyFromSeed(&seed, 0)), nil
|
||||
}
|
||||
|
||||
type NewServerParams struct {
|
||||
fx.In
|
||||
Config *config.Manager
|
||||
Logger *zap.Logger
|
||||
APIs []registry.API `group:"api"`
|
||||
}
|
||||
|
||||
func NewServer(lc fx.Lifecycle, params NewServerParams) (*http.Server, error) {
|
||||
r := registry.GetRouter()
|
||||
|
||||
r.SetConfig(params.Config)
|
||||
r.SetLogger(params.Logger)
|
||||
|
||||
for _, api := range params.APIs {
|
||||
routableAPI, ok := interface{}(api).(router.RoutableAPI)
|
||||
|
||||
if !ok {
|
||||
params.Logger.Fatal("API does not implement RoutableAPI", zap.String("api", api.Name()))
|
||||
}
|
||||
|
||||
r.RegisterAPI(routableAPI)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + strconv.FormatUint(uint64(params.Config.Config().Core.Port), 10),
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
ln, err := net.Listen("tcp", srv.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := srv.Serve(ln)
|
||||
if err != nil {
|
||||
params.Logger.Fatal("Failed to serve", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return srv.Shutdown(ctx)
|
||||
},
|
||||
})
|
||||
return srv, nil
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/http"
|
||||
|
||||
_import "git.lumeweb.com/LumeWeb/portal/import"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/mailer"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/account"
|
||||
"git.lumeweb.com/LumeWeb/portal/api"
|
||||
"git.lumeweb.com/LumeWeb/portal/cron"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
_logger "git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/metadata"
|
||||
"git.lumeweb.com/LumeWeb/portal/protocols"
|
||||
"git.lumeweb.com/LumeWeb/portal/renter"
|
||||
"git.lumeweb.com/LumeWeb/portal/storage"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxevent"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.NewManager()
|
||||
|
||||
logger, logLevel := _logger.NewLogger(cfg)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to load config", zap.Error(err))
|
||||
}
|
||||
|
||||
var fxDebug bool
|
||||
|
||||
flag.BoolVar(&fxDebug, "fx-debug", false, "Enable fx framework debug logging")
|
||||
flag.Parse()
|
||||
|
||||
var fxLogger fx.Option
|
||||
|
||||
fxLogger = fx.WithLogger(func(logger *zap.Logger) fxevent.Logger {
|
||||
log := &fxevent.ZapLogger{Logger: logger}
|
||||
log.UseLogLevel(zapcore.InfoLevel)
|
||||
log.UseErrorLevel(zapcore.ErrorLevel)
|
||||
return log
|
||||
})
|
||||
|
||||
if fxDebug {
|
||||
fxLogger = fx.Options()
|
||||
}
|
||||
|
||||
fx.New(
|
||||
fx.Supply(cfg),
|
||||
fx.Supply(logger, logLevel),
|
||||
fxLogger,
|
||||
fx.Provide(NewIdentity),
|
||||
db.Module,
|
||||
renter.Module,
|
||||
storage.Module,
|
||||
cron.Module,
|
||||
account.Module,
|
||||
metadata.Module,
|
||||
_import.Module,
|
||||
mailer.Module,
|
||||
protocols.BuildProtocols(cfg),
|
||||
api.BuildApis(cfg),
|
||||
fx.Provide(api.NewCasbin),
|
||||
fx.Invoke(protocols.SetupLifecycles),
|
||||
fx.Invoke(api.SetupLifecycles),
|
||||
fx.Provide(NewServer),
|
||||
fx.Invoke(func(*http.Server) {}),
|
||||
).Run()
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type ClusterConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Redis *RedisConfig `mapstructure:"redis"`
|
||||
Etcd *EtcdConfig `mapstructure:"etcd"`
|
||||
}
|
||||
|
||||
func clusterConfigHook() mapstructure.DecodeHookFuncType {
|
||||
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.Map || t != reflect.TypeOf(&ClusterConfig{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var clusterConfig ClusterConfig
|
||||
if err := mapstructure.Decode(data, &clusterConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the input data map includes "redis" configuration
|
||||
if opts, ok := data.(map[string]interface{})["redis"].(map[string]interface{}); ok && opts != nil {
|
||||
var redisOptions RedisConfig
|
||||
if err := mapstructure.Decode(opts, &redisOptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := redisOptions.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clusterConfig.Redis = &redisOptions
|
||||
}
|
||||
|
||||
// Check if the input data map includes "etcd" configuration
|
||||
if opts, ok := data.(map[string]interface{})["etcd"].(map[string]interface{}); ok && opts != nil {
|
||||
var etcdOptions EtcdConfig
|
||||
if err := mapstructure.Decode(opts, &etcdOptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := etcdOptions.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clusterConfig.Etcd = &etcdOptions
|
||||
}
|
||||
|
||||
return &clusterConfig, nil
|
||||
}
|
||||
}
|
291
config/config.go
291
config/config.go
|
@ -3,9 +3,10 @@ package config
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/spf13/pflag"
|
||||
"reflect"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -16,9 +17,252 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
func Init() {
|
||||
type Defaults interface {
|
||||
Defaults() map[string]interface{}
|
||||
}
|
||||
|
||||
type Validator interface {
|
||||
Validate() error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Core CoreConfig `mapstructure:"core"`
|
||||
Protocol map[string]interface{} `mapstructure:"protocol"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
viper *viper.Viper
|
||||
root *Config
|
||||
changes bool
|
||||
}
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
v, err := newConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
|
||||
m := &Manager{
|
||||
viper: v,
|
||||
root: &config,
|
||||
}
|
||||
|
||||
m.setDefaultsForObject(m.root.Core, "core")
|
||||
err = m.maybeSave()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = v.Unmarshal(&config, viper.DecodeHook(clusterConfigHook()), viper.DecodeHook(cacheConfigHook()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.validateObject(m.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.maybeConfigureCluster()
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ConfigureProtocol(name string, cfg ProtocolConfig) error {
|
||||
protocolPrefix := fmt.Sprintf("protocol.%s", name)
|
||||
|
||||
m.setDefaultsForObject(cfg, protocolPrefix)
|
||||
err := m.maybeSave()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.viper.Sub(protocolPrefix).Unmarshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.validateObject(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.root.Protocol == nil {
|
||||
m.root.Protocol = make(map[string]interface{})
|
||||
}
|
||||
|
||||
m.root.Protocol[name] = cfg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) setDefaultsForObject(obj interface{}, prefix string) {
|
||||
// Reflect on the object to traverse its fields
|
||||
objValue := reflect.ValueOf(obj)
|
||||
objType := reflect.TypeOf(obj)
|
||||
|
||||
// If the object is a pointer, we need to work with its element
|
||||
if objValue.Kind() == reflect.Ptr {
|
||||
objValue = objValue.Elem()
|
||||
objType = objType.Elem()
|
||||
}
|
||||
|
||||
// Check if the object itself implements Defaults
|
||||
if setter, ok := obj.(Defaults); ok {
|
||||
m.applyDefaults(setter, prefix)
|
||||
}
|
||||
|
||||
// Recursively handle struct fields
|
||||
for i := 0; i < objValue.NumField(); i++ {
|
||||
field := objValue.Field(i)
|
||||
fieldType := objType.Field(i)
|
||||
|
||||
// Check if the field is exported and can be interfaced
|
||||
if !field.CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
mapstructureTag := fieldType.Tag.Get("mapstructure")
|
||||
|
||||
// Construct new prefix based on the mapstructure tag, if available
|
||||
newPrefix := prefix
|
||||
if mapstructureTag != "" && mapstructureTag != "-" {
|
||||
if newPrefix != "" {
|
||||
newPrefix += "."
|
||||
}
|
||||
newPrefix += mapstructureTag
|
||||
}
|
||||
|
||||
// If field is a struct or pointer to a struct, recurse
|
||||
if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) {
|
||||
if field.Kind() == reflect.Ptr && field.IsNil() {
|
||||
// Initialize nil pointer to struct
|
||||
field.Set(reflect.New(fieldType.Type.Elem()))
|
||||
}
|
||||
m.setDefaultsForObject(field.Interface(), newPrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) validateObject(obj interface{}) error {
|
||||
// Reflect on the object to traverse its fields
|
||||
objValue := reflect.ValueOf(obj)
|
||||
objType := reflect.TypeOf(obj)
|
||||
|
||||
// If the object is a pointer, we need to work with its element
|
||||
if objValue.Kind() == reflect.Ptr {
|
||||
objValue = objValue.Elem()
|
||||
objType = objType.Elem()
|
||||
}
|
||||
|
||||
// Check if the object itself implements Defaults
|
||||
if validator, ok := obj.(Validator); ok {
|
||||
err := validator.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively handle struct fields
|
||||
for i := 0; i < objValue.NumField(); i++ {
|
||||
field := objValue.Field(i)
|
||||
fieldType := objType.Field(i)
|
||||
|
||||
if !field.CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
// If field is a struct or pointer to a struct, recurse
|
||||
if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) {
|
||||
if field.Kind() == reflect.Ptr && field.IsNil() {
|
||||
// Initialize nil pointer to struct
|
||||
field.Set(reflect.New(fieldType.Type.Elem()))
|
||||
}
|
||||
err := m.validateObject(field.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) applyDefaults(setter Defaults, prefix string) {
|
||||
defaults := setter.Defaults()
|
||||
for key, value := range defaults {
|
||||
fullKey := key
|
||||
if prefix != "" {
|
||||
fullKey = fmt.Sprintf("%s.%s", prefix, key)
|
||||
}
|
||||
if m.setDefault(fullKey, value) {
|
||||
m.changes = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) setDefault(key string, value interface{}) bool {
|
||||
if !m.viper.IsSet(key) {
|
||||
m.viper.SetDefault(key, value)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) maybeSave() error {
|
||||
if m.changes {
|
||||
ret := m.viper.WriteConfig()
|
||||
if ret != nil {
|
||||
return ret
|
||||
}
|
||||
m.changes = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) maybeConfigureCluster() error {
|
||||
if m.root.Core.Clustered != nil && m.root.Core.Clustered.Enabled {
|
||||
m.root.Core.DB.Cache.Mode = "redis"
|
||||
m.root.Core.DB.Cache.Options = m.root.Core.Clustered.Redis
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Config() *Config {
|
||||
return m.root
|
||||
}
|
||||
|
||||
func (m *Manager) Viper() *viper.Viper {
|
||||
return m.viper
|
||||
}
|
||||
|
||||
func (m *Manager) Save() error {
|
||||
err := m.viper.WriteConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.viper.Unmarshal(&m.root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newConfig() (*viper.Viper, error) {
|
||||
logger := newFallbackLogger()
|
||||
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("json")
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
for _, path := range ConfigFilePaths {
|
||||
viper.AddConfigPath(path)
|
||||
|
@ -27,33 +271,26 @@ func Init() {
|
|||
viper.SetEnvPrefix("LUME_WEB_PORTAL")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
pflag.String("database.type", "sqlite", "Database type")
|
||||
pflag.String("database.host", "localhost", "Database host")
|
||||
pflag.Int("database.port", 3306, "Database port")
|
||||
pflag.String("database.user", "root", "Database user")
|
||||
pflag.String("database.password", "", "Database password")
|
||||
pflag.String("database.name", "lumeweb_portal", "Database name")
|
||||
pflag.String("database.path", "./db.sqlite", "Database path for SQLite")
|
||||
pflag.String("renterd-api-password", ".", "admin password for renterd")
|
||||
pflag.Bool("debug", false, "enable debug mode")
|
||||
pflag.Parse()
|
||||
|
||||
err := viper.BindPFlags(pflag.CommandLine)
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Fatal error arguments: %s \n", err)
|
||||
return
|
||||
if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = viper.ReadInConfig()
|
||||
logger.Info("Config file not found, using default settings.")
|
||||
err := viper.SafeWriteConfig()
|
||||
if err != nil {
|
||||
if errors.As(err, &viper.ConfigFileNotFoundError{}) {
|
||||
// Config file not found, this is not an error.
|
||||
fmt.Println("Config file not found, using default settings.")
|
||||
} else {
|
||||
// Other error, panic.
|
||||
panic(fmt.Errorf("Fatal error config file: %s \n", err))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return viper.GetViper(), nil
|
||||
|
||||
}
|
||||
|
||||
return viper.GetViper(), nil
|
||||
}
|
||||
func newFallbackLogger() *zap.Logger {
|
||||
l, _ := zap.NewDevelopment()
|
||||
|
||||
return l
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
var _ Defaults = (*CoreConfig)(nil)
|
||||
var _ Validator = (*CoreConfig)(nil)
|
||||
|
||||
type CoreConfig struct {
|
||||
DB DatabaseConfig `mapstructure:"db"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
PortalName string `mapstructure:"portal_name"`
|
||||
ExternalPort uint `mapstructure:"external_port"`
|
||||
Identity string `mapstructure:"identity"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
Port uint `mapstructure:"port"`
|
||||
PostUploadLimit uint64 `mapstructure:"post_upload_limit"`
|
||||
Storage StorageConfig `mapstructure:"storage"`
|
||||
Protocols []string `mapstructure:"protocols"`
|
||||
Mail MailConfig `mapstructure:"mail"`
|
||||
Clustered *ClusterConfig `mapstructure:"clustered"`
|
||||
}
|
||||
|
||||
func (c CoreConfig) Validate() error {
|
||||
if c.Domain == "" {
|
||||
return errors.New("core.domain is required")
|
||||
}
|
||||
if c.PortalName == "" {
|
||||
return errors.New("core.portal_name is required")
|
||||
}
|
||||
if c.Port == 0 {
|
||||
return errors.New("core.port is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c CoreConfig) Defaults() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"post_upload_limit": units.MiB * 100,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
var _ Defaults = (*DatabaseConfig)(nil)
|
||||
var _ Validator = (*DatabaseConfig)(nil)
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Charset string `mapstructure:"charset"`
|
||||
Host string `mapstructure:"host"`
|
||||
Name string `mapstructure:"name"`
|
||||
Password string `mapstructure:"password"`
|
||||
Port int `mapstructure:"port"`
|
||||
Username string `mapstructure:"username"`
|
||||
Cache *CacheConfig `mapstructure:"cache"`
|
||||
}
|
||||
|
||||
func (d DatabaseConfig) Validate() error {
|
||||
if d.Host == "" {
|
||||
return errors.New("core.db.host is required")
|
||||
}
|
||||
if d.Port == 0 {
|
||||
return errors.New("core.db.port is required")
|
||||
}
|
||||
if d.Username == "" {
|
||||
return errors.New("core.db.username is required")
|
||||
}
|
||||
if d.Password == "" {
|
||||
return errors.New("core.db.password is required")
|
||||
}
|
||||
if d.Name == "" {
|
||||
return errors.New("core.db.name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DatabaseConfig) Defaults() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"host": "localhost",
|
||||
"charset": "utf8mb4",
|
||||
"port": 3306,
|
||||
"name": "portal",
|
||||
}
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Mode string `mapstructure:"mode"`
|
||||
Options interface{} `mapstructure:"options"`
|
||||
}
|
||||
|
||||
type MemoryConfig struct {
|
||||
}
|
||||
|
||||
func cacheConfigHook() mapstructure.DecodeHookFuncType {
|
||||
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
|
||||
// This hook is designed to operate on the options field within the CacheConfig
|
||||
if f.Kind() != reflect.Map || t != reflect.TypeOf(&CacheConfig{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var cacheConfig CacheConfig
|
||||
if err := mapstructure.Decode(data, &cacheConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Assuming the input data map includes "mode" and "options"
|
||||
switch cacheConfig.Mode {
|
||||
case "redis":
|
||||
var redisOptions RedisConfig
|
||||
if opts, ok := cacheConfig.Options.(map[string]interface{}); ok && opts != nil {
|
||||
if err := mapstructure.Decode(opts, &redisOptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheConfig.Options = redisOptions
|
||||
}
|
||||
case "memory":
|
||||
// For "memory", you might simply use an empty MemoryConfig,
|
||||
// or decode options similarly if there are any specific to memory caching.
|
||||
cacheConfig.Options = MemoryConfig{}
|
||||
case "false":
|
||||
// If "false", ensure no options are set, or set to a nil or similar neutral value.
|
||||
cacheConfig.Options = nil
|
||||
default:
|
||||
cacheConfig.Options = nil
|
||||
}
|
||||
|
||||
return cacheConfig, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package config
|
||||
|
||||
import "errors"
|
||||
|
||||
var _ Defaults = (*EtcdConfig)(nil)
|
||||
|
||||
type EtcdConfig struct {
|
||||
Endpoints []string `mapstructure:"endpoints"`
|
||||
DialTimeout int `mapstructure:"dial_timeout"`
|
||||
}
|
||||
|
||||
func (r *EtcdConfig) Validate() error {
|
||||
if len(r.Endpoints) == 0 {
|
||||
return errors.New("endpoints is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *EtcdConfig) Defaults() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"dial_timeout": 5,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package config
|
||||
|
||||
var _ Defaults = (*LogConfig)(nil)
|
||||
|
||||
type LogConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
}
|
||||
|
||||
func (l LogConfig) Defaults() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"level": "info",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var _ Validator = (*MailConfig)(nil)
|
||||
var _ Defaults = (*MailConfig)(nil)
|
||||
|
||||
type MailConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
SSL bool `mapstructure:"ssl"`
|
||||
AuthType string `mapstructure:"auth_type"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
From string `mapstructure:"from"`
|
||||
}
|
||||
|
||||
func (m MailConfig) Validate() error {
|
||||
if m.Host == "" {
|
||||
return errors.New("core.mail.host is required")
|
||||
}
|
||||
if m.Username == "" {
|
||||
return errors.New("core.mail.username is required")
|
||||
}
|
||||
if m.Password == "" {
|
||||
return errors.New("core.mail.password is required")
|
||||
}
|
||||
if m.From == "" {
|
||||
return errors.New("core.mail.from is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c MailConfig) Defaults() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"auth_type": "plain",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package config
|
||||
|
||||
type ProtocolConfig interface {
|
||||
Defaults
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package config
|
||||
|
||||
import "errors"
|
||||
|
||||
var _ Validator = (*RedisConfig)(nil)
|
||||
var _ Defaults = (*RedisConfig)(nil)
|
||||
|
||||
type RedisConfig struct {
|
||||
Address string `mapstructure:"address"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
}
|
||||
|
||||
func (r *RedisConfig) Defaults() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"address": "localhost:6379",
|
||||
"db": 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RedisConfig) Validate() error {
|
||||
if r.Address == "" {
|
||||
return errors.New("address is required")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package config
|
||||
|
||||
import "errors"
|
||||
|
||||
var _ Validator = (*DatabaseConfig)(nil)
|
||||
|
||||
type S3Config struct {
|
||||
BufferBucket string `mapstructure:"buffer_bucket"`
|
||||
Endpoint string `mapstructure:"endpoint"`
|
||||
Region string `mapstructure:"region"`
|
||||
AccessKey string `mapstructure:"access_key"`
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
}
|
||||
|
||||
func (s S3Config) Validate() error {
|
||||
if s.BufferBucket == "" {
|
||||
return errors.New("core.storage.s3.buffer_bucket is required")
|
||||
}
|
||||
if s.Endpoint == "" {
|
||||
return errors.New("core.storage.s3.endpoint is required")
|
||||
}
|
||||
if s.Region == "" {
|
||||
return errors.New("core.storage.s3.region is required")
|
||||
}
|
||||
if s.AccessKey == "" {
|
||||
return errors.New("core.storage.s3.access_key is required")
|
||||
}
|
||||
if s.SecretKey == "" {
|
||||
return errors.New("core.storage.s3.secret_key is required")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var _ Validator = (*SiaConfig)(nil)
|
||||
var _ Defaults = (*SiaConfig)(nil)
|
||||
|
||||
type SiaConfig struct {
|
||||
Key string `mapstructure:"key"`
|
||||
URL string `mapstructure:"url"`
|
||||
PriceHistoryDays uint64 `mapstructure:"price_history_days"`
|
||||
MaxUploadPrice string `mapstructure:"max_upload_price"`
|
||||
MaxDownloadPrice string `mapstructure:"max_download_price"`
|
||||
MaxStoragePrice string `mapstructure:"max_storage_price"`
|
||||
MaxContractSCPrice string `mapstructure:"max_contract_sc_price"`
|
||||
MaxRPCSCPrice string `mapstructure:"max_rpc_sc_price"`
|
||||
}
|
||||
|
||||
func (s SiaConfig) Defaults() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"max_rpc_sc_price": 0.1,
|
||||
"max_contract_sc_price": 1,
|
||||
"price_history_days": 90,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SiaConfig) Validate() error {
|
||||
if s.Key == "" {
|
||||
return errors.New("core.storage.sia.key is required")
|
||||
}
|
||||
if s.URL == "" {
|
||||
return errors.New("core.storage.sia.url is required")
|
||||
}
|
||||
|
||||
if err := validateStringNumber(s.MaxUploadPrice, "core.storage.sia.max_upload_price"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateStringNumber(s.MaxDownloadPrice, "core.storage.sia.max_download_price"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateStringNumber(s.MaxStoragePrice, "core.storage.sia.max_storage_price"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateStringNumber(s.MaxContractSCPrice, "core.storage.sia.max_contract_sc_price"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateStringNumber(s.MaxRPCSCPrice, "core.storage.sia.max_rpc_sc_price"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateStringNumber(s string, name string) error {
|
||||
if s == "" {
|
||||
return errors.New(name + " is required")
|
||||
}
|
||||
|
||||
rat, ok := new(big.Rat).SetString(s)
|
||||
if !ok {
|
||||
return errors.New("failed to parse " + name)
|
||||
}
|
||||
|
||||
if rat.Cmp(new(big.Rat).SetUint64(0)) <= 0 {
|
||||
return errors.New(name + " must be greater than 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package config
|
||||
|
||||
type StorageConfig struct {
|
||||
S3 S3Config `mapstructure:"s3"`
|
||||
Sia SiaConfig `mapstructure:"sia"`
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/account"
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
type AccountController struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
func (a *AccountController) PostRegister() {
|
||||
ri, success := tryParseRequest(request.RegisterRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := ri.(*request.RegisterRequest)
|
||||
|
||||
err := account.Register(r.Email, r.Password, r.Pubkey)
|
||||
|
||||
if err != nil {
|
||||
if err == account.ErrQueryingAcct || err == account.ErrFailedCreateAccount {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Return a success response to the client.
|
||||
a.Ctx.StatusCode(iris.StatusCreated)
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/response"
|
||||
"git.lumeweb.com/LumeWeb/portal/middleware"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
type AuthController struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token.
|
||||
func (a *AuthController) PostLogin() {
|
||||
ri, success := tryParseRequest(request.LoginRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := ri.(*request.LoginRequest)
|
||||
|
||||
token, err := auth.LoginWithPassword(r.Email, r.Password)
|
||||
|
||||
if err != nil {
|
||||
if err == auth.ErrFailedGenerateToken {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
a.respondJSON(&response.LoginResponse{Token: token})
|
||||
}
|
||||
|
||||
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
|
||||
func (a *AuthController) PostPubkeyChallenge() {
|
||||
ri, success := tryParseRequest(request.PubkeyChallengeRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := (ri).(*request.PubkeyChallengeRequest)
|
||||
|
||||
challenge, err := auth.GeneratePubkeyChallenge(r.Pubkey)
|
||||
if err != nil {
|
||||
if err == auth.ErrFailedGenerateKeyChallenge {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
a.respondJSON(&response.ChallengeResponse{Challenge: challenge})
|
||||
}
|
||||
|
||||
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
|
||||
func (a *AuthController) PostPubkeyLogin() {
|
||||
ri, success := tryParseRequest(request.PubkeyLoginRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := ri.(*request.PubkeyLoginRequest)
|
||||
|
||||
token, err := auth.LoginWithPubkey(r.Pubkey, r.Challenge, r.Signature)
|
||||
|
||||
if err != nil {
|
||||
if err == auth.ErrFailedGenerateKeyChallenge || err == auth.ErrFailedGenerateToken || err == auth.ErrFailedSaveToken {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
a.respondJSON(&response.LoginResponse{Token: token})
|
||||
|
||||
}
|
||||
|
||||
// PostLogout handles the POST /api/auth/logout request to invalidate a JWT token.
|
||||
func (a *AuthController) PostLogout() {
|
||||
ri, success := tryParseRequest(request.LogoutRequest{}, a.Ctx)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := ri.(*request.LogoutRequest)
|
||||
|
||||
err := auth.Logout(r.Token)
|
||||
|
||||
if err != nil {
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return a success response to the client.
|
||||
a.Ctx.StatusCode(iris.StatusNoContent)
|
||||
}
|
||||
|
||||
func (a *AuthController) GetStatus() {
|
||||
middleware.VerifyJwt(a.Ctx)
|
||||
|
||||
if a.Ctx.IsStopped() {
|
||||
return
|
||||
}
|
||||
|
||||
a.respondJSON(&response.AuthStatusResponse{Status: true})
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"github.com/kataras/iris/v12"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func tryParseRequest(r interface{}, ctx iris.Context) (interface{}, bool) {
|
||||
v, ok := r.(validators.Validatable)
|
||||
|
||||
if !ok {
|
||||
return r, true
|
||||
}
|
||||
|
||||
var d map[string]interface{}
|
||||
|
||||
// Read the logout request from the client.
|
||||
if err := ctx.ReadJSON(&d); err != nil {
|
||||
logger.Get().Debug("failed to parse request", zap.Error(err))
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
data, err := v.Import(d)
|
||||
if err != nil {
|
||||
logger.Get().Debug("failed to parse request", zap.Error(err))
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if err := data.Validate(); err != nil {
|
||||
logger.Get().Debug("failed to parse request", zap.Error(err))
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return data, true
|
||||
}
|
||||
|
||||
func sendErrorCustom(ctx iris.Context, err error, customError error, irisError int) bool {
|
||||
if err != nil {
|
||||
if customError != nil {
|
||||
err = customError
|
||||
}
|
||||
ctx.StopWithError(irisError, err)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
func internalError(ctx iris.Context, err error) bool {
|
||||
return sendErrorCustom(ctx, err, nil, iris.StatusInternalServerError)
|
||||
}
|
||||
func internalErrorCustom(ctx iris.Context, err error, customError error) bool {
|
||||
return sendErrorCustom(ctx, err, customError, iris.StatusInternalServerError)
|
||||
}
|
||||
func sendError(ctx iris.Context, err error, irisError int) bool {
|
||||
return sendErrorCustom(ctx, err, nil, irisError)
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
Ctx iris.Context
|
||||
}
|
||||
|
||||
func (c Controller) respondJSON(data interface{}) {
|
||||
err := c.Ctx.JSON(data)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to generate response", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getCurrentUserId(ctx iris.Context) uint {
|
||||
usr := ctx.User()
|
||||
|
||||
if usr == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
sid, _ := usr.GetID()
|
||||
userID, _ := strconv.Atoi(sid)
|
||||
|
||||
return uint(userID)
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/cid"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/response"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/middleware"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/files"
|
||||
"github.com/kataras/iris/v12"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
)
|
||||
|
||||
var errStreamDone = errors.New("done")
|
||||
|
||||
type FilesController struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
func (f *FilesController) BeginRequest(ctx iris.Context) {
|
||||
middleware.VerifyJwt(ctx)
|
||||
}
|
||||
func (f *FilesController) EndRequest(ctx iris.Context) {
|
||||
}
|
||||
|
||||
func (f *FilesController) PostUpload() {
|
||||
ctx := f.Ctx
|
||||
|
||||
file, meta, err := f.Ctx.FormFile("file")
|
||||
if internalErrorCustom(ctx, err, errors.New("invalid file data")) {
|
||||
logger.Get().Debug("invalid file data", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
upload, err := files.Upload(file, meta.Size, nil)
|
||||
|
||||
if internalError(ctx, err) {
|
||||
logger.Get().Debug("failed uploading file", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = files.Pin(upload.Hash, upload.AccountID)
|
||||
|
||||
if internalError(ctx, err) {
|
||||
logger.Get().Debug("failed pinning file", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
cidString, err := cid.EncodeString(upload.Hash, uint64(meta.Size))
|
||||
|
||||
if internalError(ctx, err) {
|
||||
logger.Get().Debug("failed creating cid", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.JSON(&response.UploadResponse{Cid: cidString})
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to create response", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilesController) GetDownloadBy(cidString string) {
|
||||
ctx := f.Ctx
|
||||
|
||||
hashHex, valid := validateCid(cidString, true, ctx)
|
||||
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
download, err := files.Download(hashHex)
|
||||
if internalError(ctx, err) {
|
||||
logger.Get().Debug("failed fetching file", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = passThroughStream(download, ctx)
|
||||
if err != errStreamDone && internalError(ctx, err) {
|
||||
logger.Get().Debug("failed streaming file", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilesController) GetProofBy(cidString string) {
|
||||
ctx := f.Ctx
|
||||
|
||||
hashHex, valid := validateCid(cidString, true, ctx)
|
||||
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
proof, err := files.DownloadProof(hashHex)
|
||||
if internalError(ctx, err) {
|
||||
logger.Get().Debug("failed fetching file proof", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = passThroughStream(proof, ctx)
|
||||
if internalError(ctx, err) {
|
||||
logger.Get().Debug("failed streaming file proof", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilesController) GetStatusBy(cidString string) {
|
||||
ctx := f.Ctx
|
||||
|
||||
hashHex, valid := validateCid(cidString, false, ctx)
|
||||
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
status := files.Status(hashHex)
|
||||
|
||||
var statusCode string
|
||||
|
||||
switch status {
|
||||
case files.STATUS_UPLOADED:
|
||||
statusCode = "uploaded"
|
||||
break
|
||||
case files.STATUS_UPLOADING:
|
||||
statusCode = "uploading"
|
||||
break
|
||||
case files.STATUS_NOT_FOUND:
|
||||
statusCode = "not_found"
|
||||
break
|
||||
}
|
||||
|
||||
f.respondJSON(&response.FileStatusResponse{Status: statusCode})
|
||||
|
||||
}
|
||||
|
||||
func (f *FilesController) PostPinBy(cidString string) {
|
||||
ctx := f.Ctx
|
||||
|
||||
hashHex, valid := validateCid(cidString, true, ctx)
|
||||
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
err := files.Pin(hashHex, getCurrentUserId(ctx))
|
||||
if internalError(ctx, err) {
|
||||
logger.Get().Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
f.Ctx.StatusCode(iris.StatusCreated)
|
||||
}
|
||||
|
||||
func (f *FilesController) GetUploadLimit() {
|
||||
f.respondJSON(&response.UploadLimit{Limit: f.Ctx.Application().ConfigurationReadOnly().GetPostMaxMemory()})
|
||||
}
|
||||
|
||||
func validateCid(cidString string, validateStatus bool, ctx iris.Context) (string, bool) {
|
||||
_, err := cid.Valid(cidString)
|
||||
if sendError(ctx, err, iris.StatusBadRequest) {
|
||||
logger.Get().Debug("invalid cid", zap.Error(err))
|
||||
return "", false
|
||||
}
|
||||
|
||||
cidObject, _ := cid.Decode(cidString)
|
||||
hashHex := cidObject.StringHash()
|
||||
|
||||
if validateStatus {
|
||||
status := files.Status(hashHex)
|
||||
|
||||
if status == files.STATUS_NOT_FOUND {
|
||||
err := errors.New("cid not found")
|
||||
sendError(ctx, errors.New("cid not found"), iris.StatusNotFound)
|
||||
logger.Get().Debug("cid not found", zap.Error(err))
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
return hashHex, true
|
||||
}
|
||||
|
||||
func passThroughStream(stream io.Reader, ctx iris.Context) error {
|
||||
closed := false
|
||||
|
||||
err := ctx.StreamWriter(func(w io.Writer) error {
|
||||
if closed {
|
||||
return errStreamDone
|
||||
}
|
||||
|
||||
count, err := io.CopyN(w, stream, 1024)
|
||||
if count == 0 || err == io.EOF {
|
||||
err = stream.(io.Closer).Close()
|
||||
if err != nil {
|
||||
logger.Get().Error("failed closing stream", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == errStreamDone {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (r LoginRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r,
|
||||
validation.Field(&r.Email, is.EmailFormat, validation.Required),
|
||||
validation.Field(&r.Password, validation.Required),
|
||||
)
|
||||
}
|
||||
func (r LoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
type LogoutRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (r LogoutRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r, validation.Field(&r.Token, validation.Required))
|
||||
}
|
||||
|
||||
func (r LogoutRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
type PubkeyChallengeRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Pubkey string `json:"pubkey"`
|
||||
}
|
||||
|
||||
func (r PubkeyChallengeRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r,
|
||||
validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)),
|
||||
)
|
||||
}
|
||||
|
||||
func (r PubkeyChallengeRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
type PubkeyLoginRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Pubkey string `json:"pubkey"`
|
||||
Challenge string `json:"challenge"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
func (r PubkeyLoginRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r,
|
||||
validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)),
|
||||
validation.Field(&r.Challenge, validation.Required),
|
||||
validation.Field(&r.Signature, validation.Required, validation.Length(128, 128)),
|
||||
)
|
||||
}
|
||||
|
||||
func (r PubkeyLoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/validators"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
)
|
||||
|
||||
type RegisterRequest struct {
|
||||
validatable validators.ValidatableImpl
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
}
|
||||
|
||||
func (r RegisterRequest) Validate() error {
|
||||
return validation.ValidateStruct(&r,
|
||||
validation.Field(&r.Email, validation.Required, is.EmailFormat),
|
||||
validation.Field(&r.Pubkey, validation.When(len(r.Password) == 0, validation.Required, validation.By(validators.CheckPubkeyValidator))),
|
||||
validation.Field(&r.Password, validation.When(len(r.Pubkey) == 0, validation.Required)),
|
||||
)
|
||||
}
|
||||
func (r RegisterRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
|
||||
return r.validatable.Import(d, r)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type AuthStatusResponse struct {
|
||||
Status bool `json:"status"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type ChallengeResponse struct {
|
||||
Challenge string `json:"challenge"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type FileStatusResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type UploadResponse struct {
|
||||
Cid string `json:"cid"`
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package response
|
||||
|
||||
type UploadLimit struct {
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package validators
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/imdario/mergo"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func CheckPubkeyValidator(value interface{}) error {
|
||||
p, _ := value.(string)
|
||||
pubkeyBytes, err := hex.DecodeString(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pubkeyBytes) != ed25519.PublicKeySize {
|
||||
return errors.New(fmt.Sprintf("pubkey must be %d bytes in hexadecimal format", ed25519.PublicKeySize))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Validatable interface {
|
||||
validation.Validatable
|
||||
Import(d map[string]interface{}) (Validatable, error)
|
||||
}
|
||||
|
||||
type ValidatableImpl struct {
|
||||
}
|
||||
|
||||
func (v ValidatableImpl) Import(d map[string]interface{}, destType Validatable) (Validatable, error) {
|
||||
instance := reflect.New(reflect.TypeOf(destType)).Interface().(Validatable)
|
||||
// Perform the import logic
|
||||
if err := mergo.Map(instance, d, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRetryLimitReached = errors.New("Retry limit reached")
|
||||
)
|
||||
|
||||
type CronService interface {
|
||||
Scheduler() gocron.Scheduler
|
||||
RegisterService(service CronableService)
|
||||
}
|
||||
|
||||
type CronableService interface {
|
||||
LoadInitialTasks(cron CronService) error
|
||||
}
|
||||
|
||||
type CronServiceParams struct {
|
||||
fx.In
|
||||
Logger *zap.Logger
|
||||
Scheduler gocron.Scheduler
|
||||
}
|
||||
|
||||
var Module = fx.Module("cron",
|
||||
fx.Options(
|
||||
fx.Provide(NewCronService),
|
||||
fx.Provide(gocron.NewScheduler),
|
||||
),
|
||||
)
|
||||
|
||||
type CronServiceDefault struct {
|
||||
scheduler gocron.Scheduler
|
||||
services []CronableService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type RetryableJobParams struct {
|
||||
Name string
|
||||
Tags []string
|
||||
Function any
|
||||
Args []any
|
||||
Attempt uint
|
||||
Limit uint
|
||||
After func(jobID uuid.UUID, jobName string)
|
||||
Error func(jobID uuid.UUID, jobName string, err error)
|
||||
}
|
||||
|
||||
type CronJob struct {
|
||||
JobId uuid.UUID
|
||||
Job gocron.JobDefinition
|
||||
Task gocron.Task
|
||||
Options []gocron.JobOption
|
||||
}
|
||||
|
||||
func (c *CronServiceDefault) Scheduler() gocron.Scheduler {
|
||||
return c.scheduler
|
||||
}
|
||||
|
||||
func NewCronService(lc fx.Lifecycle, params CronServiceParams) *CronServiceDefault {
|
||||
sc := &CronServiceDefault{
|
||||
logger: params.Logger,
|
||||
scheduler: params.Scheduler,
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return sc.start()
|
||||
},
|
||||
})
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
func (c *CronServiceDefault) start() error {
|
||||
for _, service := range c.services {
|
||||
err := service.LoadInitialTasks(c)
|
||||
if err != nil {
|
||||
c.logger.Fatal("Failed to load initial tasks for service", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
go c.scheduler.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CronServiceDefault) RegisterService(service CronableService) {
|
||||
c.services = append(c.services, service)
|
||||
}
|
||||
|
||||
func (c *CronServiceDefault) RetryableJob(params RetryableJobParams) CronJob {
|
||||
job := gocron.OneTimeJob(gocron.OneTimeJobStartImmediately())
|
||||
|
||||
if params.Attempt > 0 {
|
||||
job = gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(time.Now().Add(time.Duration(params.Attempt) * time.Minute)))
|
||||
}
|
||||
|
||||
task := gocron.NewTask(params.Function, params.Args...)
|
||||
|
||||
if params.After == nil {
|
||||
params.After = func(jobID uuid.UUID, jobName string) {}
|
||||
}
|
||||
|
||||
if params.Error == nil {
|
||||
params.Error = func(jobID uuid.UUID, jobName string, err error) {}
|
||||
}
|
||||
|
||||
listeners := gocron.WithEventListeners(gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
|
||||
params.Error(jobID, jobName, err)
|
||||
|
||||
if params.Attempt >= params.Limit && params.Limit > 0 {
|
||||
c.logger.Error("Retryable task limit reached", zap.String("jobName", jobName), zap.String("jobID", jobID.String()))
|
||||
params.Error(jobID, jobName, ErrRetryLimitReached)
|
||||
return
|
||||
}
|
||||
|
||||
taskRetry := params
|
||||
taskRetry.Attempt++
|
||||
|
||||
retryTask := c.RetryableJob(taskRetry)
|
||||
retryTask.JobId = jobID
|
||||
|
||||
_, err = c.RerunJob(retryTask)
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to create retry job", zap.Error(err))
|
||||
}
|
||||
}), gocron.AfterJobRuns(params.After))
|
||||
|
||||
name := gocron.WithName(params.Name)
|
||||
options := []gocron.JobOption{listeners, name}
|
||||
|
||||
if len(params.Tags) > 0 {
|
||||
options = append(options, gocron.WithTags(params.Tags...))
|
||||
}
|
||||
|
||||
return CronJob{
|
||||
Job: job,
|
||||
Task: task,
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CronServiceDefault) CreateJob(job CronJob) (gocron.Job, error) {
|
||||
ret, err := c.Scheduler().NewJob(job.Job, job.Task, job.Options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
func (c *CronServiceDefault) RerunJob(job CronJob) (gocron.Job, error) {
|
||||
ret, err := c.Scheduler().Update(job.JobId, job.Job, job.Task, job.Options...)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (c *CronServiceDefault) GetJobsByPrefix(prefix string) []gocron.Job {
|
||||
jobs := c.Scheduler().Jobs()
|
||||
|
||||
var ret []gocron.Job
|
||||
|
||||
for _, job := range jobs {
|
||||
if strings.HasPrefix(job.Name(), prefix) {
|
||||
ret = append(ret, job)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *CronServiceDefault) GetJobByName(name string) gocron.Job {
|
||||
jobs := c.Scheduler().Jobs()
|
||||
|
||||
for _, job := range jobs {
|
||||
if job.Name() == name {
|
||||
return job
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CronServiceDefault) GetJobByID(id uuid.UUID) gocron.Job {
|
||||
jobs := c.Scheduler().Jobs()
|
||||
|
||||
for _, job := range jobs {
|
||||
if job.ID() == id {
|
||||
return job
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/go-gorm/caches/v4"
|
||||
)
|
||||
|
||||
type memoryCacher struct {
|
||||
store *sync.Map
|
||||
}
|
||||
|
||||
func (c *memoryCacher) init() {
|
||||
if c.store == nil {
|
||||
c.store = &sync.Map{}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *memoryCacher) Get(ctx context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) {
|
||||
c.init()
|
||||
val, ok := c.store.Load(key)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := q.Unmarshal(val.([]byte)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (c *memoryCacher) Store(ctx context.Context, key string, val *caches.Query[any]) error {
|
||||
c.init()
|
||||
res, err := val.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.store.Store(key, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *memoryCacher) Invalidate(ctx context.Context) error {
|
||||
c.store = &sync.Map{}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-gorm/caches/v4"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type redisCacher struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func (c *redisCacher) Get(ctx context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) {
|
||||
res, err := c.rdb.Get(ctx, key).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := q.Unmarshal([]byte(res)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (c *redisCacher) Store(ctx context.Context, key string, val *caches.Query[any]) error {
|
||||
res, err := val.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.rdb.Set(ctx, key, res, 300*time.Second) // Set proper cache time
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *redisCacher) Invalidate(ctx context.Context) error {
|
||||
var (
|
||||
cursor uint64
|
||||
keys []string
|
||||
)
|
||||
for {
|
||||
var (
|
||||
k []string
|
||||
err error
|
||||
)
|
||||
k, cursor, err = c.rdb.Scan(ctx, cursor, fmt.Sprintf("%s*", caches.IdentifierPrefix), 0).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keys = append(keys, k...)
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
if _, err := c.rdb.Del(ctx, keys...).Result(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
141
db/db.go
141
db/db.go
|
@ -1,65 +1,116 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/db/models"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"github.com/go-gorm/caches/v4"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Declare a global variable to hold the database connection.
|
||||
var db *gorm.DB
|
||||
|
||||
// Init initializes the database connection based on the app's configuration settings.
|
||||
func Init() {
|
||||
// If the database connection has already been initialized, panic.
|
||||
if db != nil {
|
||||
panic("DB already initialized")
|
||||
type DatabaseParams struct {
|
||||
fx.In
|
||||
Config *config.Manager
|
||||
Logger *zap.Logger
|
||||
LoggerLevel *zap.AtomicLevel
|
||||
}
|
||||
|
||||
// Retrieve database connection settings from the app's configuration using the viper library.
|
||||
dbType := viper.GetString("database.type")
|
||||
dbHost := viper.GetString("database.host")
|
||||
dbPort := viper.GetInt("database.port")
|
||||
dbSocket := viper.GetString("database.socket")
|
||||
dbUser := viper.GetString("database.user")
|
||||
dbPassword := viper.GetString("database.password")
|
||||
dbName := viper.GetString("database.name")
|
||||
dbPath := viper.GetString("database.path")
|
||||
var Module = fx.Module("db",
|
||||
fx.Options(
|
||||
fx.Provide(NewDatabase),
|
||||
),
|
||||
)
|
||||
|
||||
var err error
|
||||
var dsn string
|
||||
switch dbType {
|
||||
// Connect to a MySQL database.
|
||||
case "mysql":
|
||||
if dbSocket != "" {
|
||||
dsn = fmt.Sprintf("%s:%s@unix(%s)/%s", dbUser, dbPassword, dbSocket, dbName)
|
||||
} else {
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", dbUser, dbPassword, dbHost, dbPort, dbName)
|
||||
}
|
||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
// Connect to a SQLite database.
|
||||
case "sqlite":
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
// If the database type is unsupported, panic.
|
||||
default:
|
||||
panic(fmt.Errorf("Unsupported database type: %s \n", dbType))
|
||||
}
|
||||
// If there was an error connecting to the database, panic.
|
||||
func NewDatabase(lc fx.Lifecycle, params DatabaseParams) *gorm.DB {
|
||||
username := params.Config.Config().Core.DB.Username
|
||||
password := params.Config.Config().Core.DB.Password
|
||||
host := params.Config.Config().Core.DB.Host
|
||||
port := params.Config.Config().Core.DB.Port
|
||||
dbname := params.Config.Config().Core.DB.Name
|
||||
charset := params.Config.Config().Core.DB.Charset
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", username, password, host, port, dbname, charset)
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: newLogger(params.Logger, params.LoggerLevel),
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Failed to connect to database: %s \n", err))
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Automatically migrate the database schema based on the model definitions.
|
||||
err = db.Migrator().AutoMigrate(&model.Account{}, &model.Key{}, &model.KeyChallenge{}, &model.LoginSession{}, &model.Upload{}, &model.Pin{}, &model.Tus{})
|
||||
cacher := getCacher(params.Config, params.Logger)
|
||||
if cacher != nil {
|
||||
cache := &caches.Caches{Conf: &caches.Config{
|
||||
Cacher: cacher,
|
||||
}}
|
||||
err := db.Use(cache)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Database setup failed database type: %s \n", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the database connection instance.
|
||||
func Get() *gorm.DB {
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return db.AutoMigrate(models.GetModels()...)
|
||||
},
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func getCacheMode(cm *config.Manager, logger *zap.Logger) string {
|
||||
|
||||
if cm.Config().Core.DB.Cache == nil {
|
||||
return "none"
|
||||
}
|
||||
|
||||
switch cm.Config().Core.DB.Cache.Mode {
|
||||
case "", "none":
|
||||
return "none"
|
||||
case "memory":
|
||||
return "memory"
|
||||
case "redis":
|
||||
return "redis"
|
||||
default:
|
||||
logger.Fatal("invalid cache mode", zap.String("mode", cm.Config().Core.DB.Cache.Mode))
|
||||
}
|
||||
|
||||
return "none"
|
||||
}
|
||||
|
||||
func getCacher(cm *config.Manager, logger *zap.Logger) caches.Cacher {
|
||||
mode := getCacheMode(cm, logger)
|
||||
|
||||
switch mode {
|
||||
case "none":
|
||||
return nil
|
||||
|
||||
case "memory":
|
||||
return &memoryCacher{}
|
||||
case "redis":
|
||||
rcfg, ok := cm.Config().Core.DB.Cache.Options.(config.RedisConfig)
|
||||
if !ok {
|
||||
logger.Fatal("invalid redis config")
|
||||
return nil
|
||||
}
|
||||
return &redisCacher{
|
||||
redis.NewClient(&redis.Options{
|
||||
Addr: rcfg.Address,
|
||||
Password: rcfg.Password,
|
||||
DB: rcfg.DB,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go.uber.org/zap"
|
||||
dbLogger "gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var _ dbLogger.Interface = (*logger)(nil)
|
||||
|
||||
var (
|
||||
levels = map[dbLogger.LogLevel]zap.AtomicLevel{
|
||||
dbLogger.Silent: zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
dbLogger.Error: zap.NewAtomicLevelAt(zap.ErrorLevel),
|
||||
dbLogger.Warn: zap.NewAtomicLevelAt(zap.WarnLevel),
|
||||
dbLogger.Info: zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
}
|
||||
)
|
||||
|
||||
type logger struct {
|
||||
logger *zap.Logger
|
||||
level *zap.AtomicLevel
|
||||
}
|
||||
|
||||
func (l logger) LogMode(level dbLogger.LogLevel) dbLogger.Interface {
|
||||
if atomicLevel, ok := levels[level]; ok {
|
||||
l.level.SetLevel(atomicLevel.Level())
|
||||
return l
|
||||
}
|
||||
|
||||
l.logger.Fatal("invalid log level", zap.Int("level", int(level)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l logger) Info(ctx context.Context, s string, i ...interface{}) {
|
||||
l.logger.Info(s, interfacesToFields(i...)...)
|
||||
}
|
||||
|
||||
func (l logger) Warn(ctx context.Context, s string, i ...interface{}) {
|
||||
l.logger.Warn(s, interfacesToFields(i...)...)
|
||||
}
|
||||
|
||||
func (l logger) Error(ctx context.Context, s string, i ...interface{}) {
|
||||
l.logger.Error(s, interfacesToFields(i...)...)
|
||||
}
|
||||
|
||||
func (l logger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
if l.level.Level() <= zap.DebugLevel {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return
|
||||
}
|
||||
|
||||
sql, rowsAffected := fc()
|
||||
fields := []zap.Field{
|
||||
zap.String("sql", sql),
|
||||
zap.Int64("rows_affected", rowsAffected),
|
||||
zap.Duration("elapsed", time.Since(begin)),
|
||||
}
|
||||
if err != nil {
|
||||
fields = append(fields, zap.Error(err))
|
||||
}
|
||||
l.logger.Debug("trace", fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func newLogger(zlog *zap.Logger, zlogLevel *zap.AtomicLevel) *logger {
|
||||
return &logger{logger: zlog, level: zlogLevel}
|
||||
}
|
||||
|
||||
func interfacesToFields(i ...interface{}) []zap.Field {
|
||||
fields := make([]zap.Field, 0)
|
||||
for idx, v := range i {
|
||||
fields = append(fields, zap.Any(strconv.Itoa(idx), v))
|
||||
}
|
||||
return fields
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&APIKey{})
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
Key string
|
||||
User User
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerModel(&Blocklist{})
|
||||
}
|
||||
|
||||
type Blocklist struct {
|
||||
gorm.Model
|
||||
IP string
|
||||
Reason string
|
||||
BlockedAt time.Time
|
||||
}
|
||||
|
||||
func (Blocklist) TableName() string {
|
||||
return "blocklist"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&DNSLink{})
|
||||
}
|
||||
|
||||
type DNSLink struct {
|
||||
gorm.Model
|
||||
UserID uint `gorm:"uniqueIndex:idx_user_id_upload"`
|
||||
User User
|
||||
UploadID uint `gorm:"uniqueIndex:idx_user_id_upload"`
|
||||
Upload Upload
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerModel(&Download{})
|
||||
}
|
||||
|
||||
type Download struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
User User
|
||||
UploadID uint
|
||||
Upload Upload
|
||||
DownloadedAt time.Time
|
||||
IP string
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerModel(&EmailVerification{})
|
||||
}
|
||||
|
||||
type EmailVerification struct {
|
||||
gorm.Model
|
||||
|
||||
UserID uint
|
||||
User User
|
||||
NewEmail string
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ImportStatus string
|
||||
|
||||
const (
|
||||
ImportStatusQueued ImportStatus = "queued"
|
||||
ImportStatusProcessing ImportStatus = "processing"
|
||||
ImportStatusCompleted ImportStatus = "completed"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerModel(&Import{})
|
||||
}
|
||||
|
||||
type Import struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
Hash []byte `gorm:"type:binary(32);"`
|
||||
Protocol string
|
||||
User User
|
||||
ImporterIP string
|
||||
Status ImportStatus
|
||||
Progress float64
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package models
|
||||
|
||||
var registered []interface{}
|
||||
|
||||
func registerModel(model interface{}) {
|
||||
registered = append(registered, model)
|
||||
}
|
||||
|
||||
func GetModels() []interface{} {
|
||||
return registered
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerModel(&PasswordReset{})
|
||||
}
|
||||
|
||||
type PasswordReset struct {
|
||||
gorm.Model
|
||||
|
||||
UserID uint
|
||||
User User
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&Pin{})
|
||||
}
|
||||
|
||||
type Pin struct {
|
||||
gorm.Model
|
||||
UploadID uint
|
||||
Upload Upload
|
||||
UserID uint
|
||||
User User
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&PublicKey{})
|
||||
}
|
||||
|
||||
type PublicKey struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
Key string `gorm:"unique;not null"`
|
||||
User User
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&S3Upload{})
|
||||
}
|
||||
|
||||
type S3Upload struct {
|
||||
gorm.Model
|
||||
UploadID string `gorm:"unique;not null"`
|
||||
Bucket string `gorm:"not null;index:idx_bucket_key"`
|
||||
Key string `gorm:"not null;index:idx_bucket_key"`
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//go:build s5
|
||||
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&S5Challenge{})
|
||||
}
|
||||
|
||||
type S5Challenge struct {
|
||||
gorm.Model
|
||||
Challenge string
|
||||
Pubkey string
|
||||
Type string
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
var _ schema.Tabler = (*SCPriceHistory)(nil)
|
||||
|
||||
func init() {
|
||||
registerModel(&SCPriceHistory{})
|
||||
}
|
||||
|
||||
type SCPriceHistory struct {
|
||||
gorm.Model
|
||||
CreatedAt time.Time `gorm:"index:idx_rate"`
|
||||
Rate decimal.Decimal `gorm:"type:DECIMAL(30,20);index:idx_rate"`
|
||||
}
|
||||
|
||||
func (SCPriceHistory) TableName() string {
|
||||
return "sc_price_history"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&SiaUpload{})
|
||||
}
|
||||
|
||||
type SiaUpload struct {
|
||||
gorm.Model
|
||||
UploadID string `gorm:"unique;not null"`
|
||||
Bucket string `gorm:"not null;index:idx_bucket_key"`
|
||||
Key string `gorm:"not null;index:idx_bucket_key"`
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerModel(&TusLock{})
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTusLockBusy = errors.New("lock is currently held by another process")
|
||||
)
|
||||
|
||||
type TusLock struct {
|
||||
gorm.Model
|
||||
LockId string `gorm:"index:idx_lock_id,unique"`
|
||||
HolderPID int `gorm:"index"`
|
||||
AcquiredAt time.Time
|
||||
ExpiresAt time.Time
|
||||
ReleaseRequested bool
|
||||
DeletedAt gorm.DeletedAt `gorm:"index:idx_lock_id,unique"`
|
||||
}
|
||||
|
||||
func (t *TusLock) TryLock(db *gorm.DB, ctx context.Context) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
var existingLock TusLock
|
||||
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("lock_id = ?", t.LockId).First(&existingLock).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Insert new lock record
|
||||
err := tx.WithContext(ctx).Create(t).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t = &existingLock
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if existing lock is expired
|
||||
if existingLock.ExpiresAt.Before(time.Now()) || existingLock.ReleaseRequested {
|
||||
|
||||
err := tx.Model(&existingLock).Updates(t).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t = &existingLock
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lock is currently held by another process
|
||||
return ErrTusLockBusy
|
||||
})
|
||||
}
|
||||
func (t *TusLock) RequestRelease(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// Update the ReleaseRequested flag in the database for the specific lock
|
||||
return tx.Model(t).Where("lock_id = ?", t.LockId).Update("release_requested", true).Error
|
||||
})
|
||||
}
|
||||
func (t *TusLock) Released(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// Update the ReleaseRequested flag in the database for the specific lock
|
||||
return tx.Model(t).Where("lock_id = ?", t.LockId).Update("release_requested", false).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (t *TusLock) IsReleaseRequested(db *gorm.DB) (bool, error) {
|
||||
var count int64
|
||||
err := db.Model(&TusLock{}).Where(&TusLock{LockId: t.LockId, ReleaseRequested: true}).Count(&count).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
func (t *TusLock) Delete(db *gorm.DB) error {
|
||||
return db.Where("lock_id = ?", t.LockId).Delete(&TusLock{}).Error
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&TusUpload{})
|
||||
|
||||
}
|
||||
|
||||
type TusUpload struct {
|
||||
gorm.Model
|
||||
Hash []byte `gorm:"type:binary(32);uniqueIndex:idx_hash_deleted"`
|
||||
MimeType string
|
||||
UploadID string `gorm:"uniqueIndex"`
|
||||
UploaderID uint
|
||||
UploaderIP string
|
||||
Uploader User `gorm:"foreignKey:UploaderID"`
|
||||
Protocol string
|
||||
Completed bool
|
||||
DeletedAt gorm.DeletedAt `gorm:"uniqueIndex:idx_hash_deleted"`
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
func init() {
|
||||
registerModel(&Upload{})
|
||||
}
|
||||
|
||||
type Upload struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
Hash []byte `gorm:"type:binary(32);uniqueIndex"`
|
||||
MimeType string
|
||||
Protocol string
|
||||
User User
|
||||
UploaderIP string
|
||||
Size uint64
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
emailverifier "github.com/AfterShip/email-verifier"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerModel(&User{})
|
||||
}
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string `gorm:"unique"`
|
||||
PasswordHash string
|
||||
Role string
|
||||
PublicKeys []PublicKey
|
||||
APIKeys []APIKey
|
||||
Uploads []Upload
|
||||
LastLogin *time.Time
|
||||
LastLoginIP string
|
||||
OTPEnabled bool `gorm:"default:false;"`
|
||||
OTPVerified bool `gorm:"default:false;"`
|
||||
OTPSecret string
|
||||
OTPAuthUrl string
|
||||
Verified bool `gorm:"default:false;"`
|
||||
EmailVerifications []EmailVerification
|
||||
PasswordResets []PasswordReset
|
||||
}
|
||||
|
||||
func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
||||
dest := tx.Statement.Dest.(User)
|
||||
|
||||
if tx.Statement.Changed("Email") {
|
||||
verify, err := getEmailVerfier().Verify(dest.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !verify.Syntax.Valid {
|
||||
return errors.New("email is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEmailVerfier() *emailverifier.Verifier {
|
||||
verifier := emailverifier.NewVerifier()
|
||||
|
||||
verifier.DisableSMTPCheck()
|
||||
verifier.DisableGravatarCheck()
|
||||
verifier.DisableDomainSuggest()
|
||||
verifier.DisableAutoUpdateDisposable()
|
||||
|
||||
return verifier
|
||||
}
|
241
go.mod
241
go.mod
|
@ -1,127 +1,152 @@
|
|||
module git.lumeweb.com/LumeWeb/portal
|
||||
|
||||
go 1.18
|
||||
go 1.21.6
|
||||
|
||||
require (
|
||||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
github.com/golang-queue/queue v0.1.3
|
||||
github.com/iris-contrib/swagger v0.0.0-20230311205341-32127a753a68
|
||||
github.com/joomcode/errorx v1.1.0
|
||||
github.com/kataras/iris/v12 v12.2.0
|
||||
github.com/kataras/jwt v0.1.8
|
||||
github.com/multiformats/go-multibase v0.2.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/swaggo/swag v1.16.1
|
||||
github.com/tus/tusd v1.11.0
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
gorm.io/driver/mysql v1.4.6
|
||||
gorm.io/driver/sqlite v1.4.3
|
||||
gorm.io/gorm v1.24.3
|
||||
lukechampine.com/blake3 v1.2.1
|
||||
git.lumeweb.com/LumeWeb/libs5-go v0.0.0-20240314105331-6510beddf2cf
|
||||
github.com/AfterShip/email-verifier v1.4.0
|
||||
github.com/LumeWeb/siacentral-api v0.0.0-20240311114304-4ff40c07bce5
|
||||
github.com/aws/aws-sdk-go-v2 v1.25.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.2
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.2
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3
|
||||
github.com/casbin/casbin/v2 v2.82.0
|
||||
github.com/ddo/rq v0.0.0-20190828174524-b3daa55fcaba
|
||||
github.com/dnslink-std/go v0.6.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/getkin/kin-openapi v0.118.0
|
||||
github.com/go-co-op/gocron/v2 v2.2.4
|
||||
github.com/go-gorm/caches/v4 v4.0.0
|
||||
github.com/go-resty/resty/v2 v2.11.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/golang-queue/queue v0.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-plugin v1.6.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/rs/cors v1.10.1
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/tus/tusd/v2 v2.2.3-0.20240125123123-9080d351525d
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
github.com/wneessen/go-mail v0.4.1
|
||||
go.sia.tech/core v0.1.12
|
||||
go.sia.tech/jape v0.11.2-0.20240228204811-29a0f056d231
|
||||
go.sia.tech/renterd v1.0.5
|
||||
go.uber.org/fx v1.20.1
|
||||
go.uber.org/zap v1.26.0
|
||||
go.uber.org/zap/exp v0.2.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
gorm.io/driver/mysql v1.5.4
|
||||
gorm.io/gorm v1.25.7
|
||||
lukechampine.com/blake3 v1.2.2-0.20240329192137-af604d0fbc33
|
||||
nhooyr.io/websocket v1.8.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
|
||||
github.com/Joker/jade v1.1.3 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/flosch/pongo2/v4 v4.0.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.9 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.2.0 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.19.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.27.2 // indirect
|
||||
github.com/aws/smithy-go v1.20.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/casbin/govaluate v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dchest/threefish v0.0.0-20120919164726-3ecf4c494abf // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.14.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.8 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect
|
||||
github.com/iris-contrib/schema v0.0.6 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/hbollon/go-edlib v1.6.0 // indirect
|
||||
github.com/invopop/yaml v0.2.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kataras/blocks v0.0.7 // indirect
|
||||
github.com/kataras/golog v0.1.8 // indirect
|
||||
github.com/kataras/neffos v0.0.21 // indirect
|
||||
github.com/kataras/pio v0.0.11 // indirect
|
||||
github.com/kataras/sitemap v0.0.6 // indirect
|
||||
github.com/kataras/tunnel v0.0.4 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/reedsolomon v1.12.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mediocregopher/radix/v3 v3.8.1 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.23 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/nats-io/nats.go v1.25.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.4 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.12.5 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.6.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/mr-tron/base58 v1.1.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.0.3 // indirect
|
||||
github.com/multiformats/go-base36 v0.1.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/oklog/run v1.0.0 // indirect
|
||||
github.com/olebedev/emitter v0.0.0-20230411050614-349169dec2ba // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/prometheus/client_golang v1.18.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/yosssi/ace v0.0.5 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
gitlab.com/NebulousLabs/bolt v1.4.4 // indirect
|
||||
gitlab.com/NebulousLabs/encoding v0.0.0-20200604091946-456c3dc907fe // indirect
|
||||
gitlab.com/NebulousLabs/entropy-mnemonics v0.0.0-20181018051301-7532f67e3500 // indirect
|
||||
gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975 // indirect
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
|
||||
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6 // indirect
|
||||
gitlab.com/NebulousLabs/log v0.0.0-20210609172545-77f6775350e2 // indirect
|
||||
gitlab.com/NebulousLabs/merkletree v0.0.0-20200118113624-07fbf710afc4 // indirect
|
||||
gitlab.com/NebulousLabs/persist v0.0.0-20200605115618-007e5e23d877 // indirect
|
||||
gitlab.com/NebulousLabs/ratelimit v0.0.0-20200811080431-99b8f0768b2e // indirect
|
||||
gitlab.com/NebulousLabs/siamux v0.0.2-0.20220630142132-142a1443a259 // indirect
|
||||
gitlab.com/NebulousLabs/threadgroup v0.0.0-20200608151952-38921fbef213 // indirect
|
||||
go.etcd.io/bbolt v1.3.8 // indirect
|
||||
go.sia.tech/mux v1.2.0 // indirect
|
||||
go.sia.tech/siad v1.5.10-0.20230228235644-3059c0b930ca // indirect
|
||||
go.uber.org/dig v1.17.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
|
||||
google.golang.org/grpc v1.62.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/frand v1.4.2 // indirect
|
||||
)
|
||||
|
||||
replace go.uber.org/multierr => go.uber.org/multierr v1.9.0
|
||||
|
||||
replace (
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.39.0
|
||||
go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry => go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.12.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace => go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.12.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.12.0
|
||||
go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v0.37.0
|
||||
go.opentelemetry.io/otel/sdk => go.opentelemetry.io/otel/sdk v1.12.0
|
||||
go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.14.0
|
||||
go.opentelemetry.io/proto/otlp => go.opentelemetry.io/proto/otlp v0.19.0
|
||||
)
|
||||
replace github.com/tus/tusd/v2 => github.com/LumeWeb/tusd/v2 v2.2.3-0.20240224143554-96925dd43120
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
package _import
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/db/models"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var ErrNotFound = gorm.ErrRecordNotFound
|
||||
|
||||
var _ ImportService = (*ImportServiceDefault)(nil)
|
||||
|
||||
type ImportMetadata struct {
|
||||
ID uint
|
||||
UserID uint
|
||||
Hash []byte
|
||||
Status models.ImportStatus
|
||||
Progress float64
|
||||
Protocol string
|
||||
ImporterIP string
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type ImportService interface {
|
||||
SaveImport(ctx context.Context, metadata ImportMetadata, skipExisting bool) error
|
||||
GetImport(ctx context.Context, objectHash []byte) (ImportMetadata, error)
|
||||
DeleteImport(ctx context.Context, objectHash []byte) error
|
||||
UpdateProgress(ctx context.Context, objectHash []byte, stage int, totalStages int) error
|
||||
UpdateStatus(ctx context.Context, objectHash []byte, status models.ImportStatus) error
|
||||
}
|
||||
|
||||
func (u ImportMetadata) IsEmpty() bool {
|
||||
if u.UserID != 0 || u.Protocol != "" || u.ImporterIP != "" || u.Status != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !u.Created.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(u.Hash) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var Module = fx.Module("import",
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
NewImportService,
|
||||
fx.As(new(ImportService)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
type ImportServiceDefault struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (i ImportServiceDefault) UpdateProgress(ctx context.Context, objectHash []byte, stage int, totalStages int) error {
|
||||
_import, err := i.GetImport(ctx, objectHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _import.IsEmpty() {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
_import.Progress = float64(stage) / float64(totalStages) * 100.0
|
||||
|
||||
return i.SaveImport(ctx, _import, false)
|
||||
}
|
||||
|
||||
func (i ImportServiceDefault) UpdateStatus(ctx context.Context, objectHash []byte, status models.ImportStatus) error {
|
||||
_import, err := i.GetImport(ctx, objectHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _import.IsEmpty() {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
_import.Status = status
|
||||
|
||||
return i.SaveImport(ctx, _import, false)
|
||||
}
|
||||
|
||||
func (i ImportServiceDefault) SaveImport(ctx context.Context, metadata ImportMetadata, skipExisting bool) error {
|
||||
var __import models.Import
|
||||
|
||||
__import.Hash = metadata.Hash
|
||||
|
||||
ret := i.db.WithContext(ctx).Model(&models.Import{}).Where(&__import).First(&__import)
|
||||
|
||||
if ret.Error != nil {
|
||||
if errors.Is(ret.Error, gorm.ErrRecordNotFound) {
|
||||
return i.createImport(ctx, metadata)
|
||||
}
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
if skipExisting {
|
||||
return nil
|
||||
}
|
||||
|
||||
changed := false
|
||||
|
||||
if __import.UserID != metadata.UserID {
|
||||
__import.UserID = metadata.UserID
|
||||
changed = true
|
||||
}
|
||||
|
||||
if __import.Status != metadata.Status {
|
||||
__import.Status = metadata.Status
|
||||
changed = true
|
||||
}
|
||||
|
||||
if __import.Progress != metadata.Progress {
|
||||
__import.Progress = metadata.Progress
|
||||
changed = true
|
||||
}
|
||||
|
||||
if __import.Protocol != metadata.Protocol {
|
||||
__import.Protocol = metadata.Protocol
|
||||
changed = true
|
||||
}
|
||||
|
||||
if __import.ImporterIP != metadata.ImporterIP {
|
||||
__import.ImporterIP = metadata.ImporterIP
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
return i.db.Updates(&__import).Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ImportServiceDefault) createImport(ctx context.Context, metadata ImportMetadata) error {
|
||||
__import := models.Import{
|
||||
UserID: metadata.UserID,
|
||||
Hash: metadata.Hash,
|
||||
Status: metadata.Status,
|
||||
Progress: metadata.Progress,
|
||||
Protocol: metadata.Protocol,
|
||||
ImporterIP: metadata.ImporterIP,
|
||||
}
|
||||
|
||||
if __import.Status == "" {
|
||||
__import.Status = models.ImportStatusQueued
|
||||
}
|
||||
|
||||
return m.db.WithContext(ctx).Create(&__import).Error
|
||||
}
|
||||
|
||||
func (i ImportServiceDefault) GetImport(ctx context.Context, objectHash []byte) (ImportMetadata, error) {
|
||||
var _import models.Import
|
||||
|
||||
_import.Hash = objectHash
|
||||
|
||||
ret := i.db.WithContext(ctx).Model(&models.Import{}).Where(&_import).First(&_import)
|
||||
|
||||
if ret.Error != nil {
|
||||
if errors.Is(ret.Error, gorm.ErrRecordNotFound) {
|
||||
return ImportMetadata{}, ErrNotFound
|
||||
}
|
||||
return ImportMetadata{}, ret.Error
|
||||
|
||||
}
|
||||
|
||||
return ImportMetadata{
|
||||
ID: _import.ID,
|
||||
UserID: _import.UserID,
|
||||
Hash: _import.Hash,
|
||||
Protocol: _import.Protocol,
|
||||
Status: _import.Status,
|
||||
Progress: _import.Progress,
|
||||
ImporterIP: _import.ImporterIP,
|
||||
Created: _import.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i ImportServiceDefault) DeleteImport(ctx context.Context, objectHash []byte) error {
|
||||
var _import models.Import
|
||||
|
||||
_import.Hash = objectHash
|
||||
|
||||
ret := i.db.WithContext(ctx).Model(&models.Import{}).Where(&_import).Delete(&_import)
|
||||
|
||||
if ret.Error != nil {
|
||||
if errors.Is(ret.Error, gorm.ErrRecordNotFound) {
|
||||
return ErrNotFound
|
||||
}
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImportServiceParams struct {
|
||||
fx.In
|
||||
Db *gorm.DB
|
||||
}
|
||||
|
||||
func NewImportService(params ImportServiceParams) *ImportServiceDefault {
|
||||
return &ImportServiceDefault{
|
||||
db: params.Db,
|
||||
}
|
||||
}
|
|
@ -1,30 +1,45 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"log"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
func NewLogger(cm *config.Manager) (*zap.Logger, *zap.AtomicLevel) {
|
||||
|
||||
func Init() {
|
||||
var newLogger *zap.Logger
|
||||
var err error
|
||||
// Create a new atomic level
|
||||
atomicLevel := zap.NewAtomicLevel()
|
||||
|
||||
if viper.GetBool("debug") {
|
||||
newLogger, err = zap.NewDevelopment()
|
||||
if cm != nil {
|
||||
// Set initial log level, for example, info level
|
||||
atomicLevel.SetLevel(mapLogLevel(cm.Config().Core.Log.Level))
|
||||
} else {
|
||||
newLogger, err = zap.NewProduction()
|
||||
atomicLevel.SetLevel(mapLogLevel("debug"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
// Create the logger with the atomic level
|
||||
logger := zap.New(zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
|
||||
zapcore.Lock(os.Stdout),
|
||||
atomicLevel,
|
||||
), zap.AddCaller())
|
||||
|
||||
return logger, &atomicLevel
|
||||
}
|
||||
|
||||
logger = newLogger
|
||||
func mapLogLevel(level string) zapcore.Level {
|
||||
switch level {
|
||||
case "debug":
|
||||
return zapcore.DebugLevel
|
||||
case "info":
|
||||
return zapcore.InfoLevel
|
||||
case "warn":
|
||||
return zapcore.WarnLevel
|
||||
default:
|
||||
return zapcore.ErrorLevel
|
||||
}
|
||||
|
||||
func Get() *zap.Logger {
|
||||
return logger
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package mailer
|
||||
|
||||
import "github.com/wneessen/go-mail"
|
||||
|
||||
type Email struct {
|
||||
to string
|
||||
from string
|
||||
subject string
|
||||
body string
|
||||
}
|
||||
|
||||
func (e *Email) To() string {
|
||||
return e.to
|
||||
}
|
||||
|
||||
func (e *Email) SetTo(to string) {
|
||||
e.to = to
|
||||
}
|
||||
|
||||
func (e *Email) From() string {
|
||||
return e.from
|
||||
}
|
||||
|
||||
func (e *Email) SetFrom(from string) {
|
||||
e.from = from
|
||||
}
|
||||
|
||||
func (e *Email) Subject() string {
|
||||
return e.subject
|
||||
}
|
||||
|
||||
func (e *Email) SetSubject(subject string) {
|
||||
e.subject = subject
|
||||
}
|
||||
|
||||
func (e *Email) Body() string {
|
||||
return e.body
|
||||
}
|
||||
|
||||
func (e *Email) SetBody(body string) {
|
||||
e.body = body
|
||||
}
|
||||
|
||||
func (e *Email) ToMessage() (*mail.Msg, error) {
|
||||
msg :=
|
||||
mail.NewMsg()
|
||||
|
||||
err := msg.From(e.from)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = msg.To(e.to)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg.SetBodyString("text/plain", e.body)
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func NewEmail(subject, body string) *Email {
|
||||
return &Email{
|
||||
subject: subject,
|
||||
body: body,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
"github.com/wneessen/go-mail"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TemplateData = map[string]interface{}
|
||||
|
||||
var Module = fx.Module("mailer",
|
||||
fx.Options(
|
||||
fx.Provide(NewMailer),
|
||||
fx.Provide(NewTemplateRegistry),
|
||||
fx.Invoke(func(registry *TemplateRegistry) error {
|
||||
return registry.loadTemplates()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
type Mailer struct {
|
||||
config *config.Manager
|
||||
logger *zap.Logger
|
||||
client *mail.Client
|
||||
templateRegistry *TemplateRegistry
|
||||
}
|
||||
|
||||
func (m *Mailer) TemplateSend(template string, subjectVars TemplateData, bodyVars TemplateData, to string) error {
|
||||
email, err := m.templateRegistry.RenderTemplate(template, subjectVars, bodyVars)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
email.SetFrom(m.config.Config().Core.Mail.From)
|
||||
email.SetTo(to)
|
||||
|
||||
msg, err := email.ToMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.client.DialAndSend(msg)
|
||||
}
|
||||
|
||||
func NewMailer(lc fx.Lifecycle, config *config.Manager, logger *zap.Logger, templateRegistry *TemplateRegistry) (*Mailer, error) {
|
||||
m := &Mailer{config: config, logger: logger, templateRegistry: templateRegistry}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(context.Context) error {
|
||||
var options []mail.Option
|
||||
|
||||
if config.Config().Core.Mail.Port != 0 {
|
||||
options = append(options, mail.WithPort(config.Config().Core.Mail.Port))
|
||||
}
|
||||
|
||||
if config.Config().Core.Mail.AuthType != "" {
|
||||
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthType(strings.ToUpper(config.Config().Core.Mail.AuthType))))
|
||||
}
|
||||
|
||||
if config.Config().Core.Mail.SSL {
|
||||
options = append(options, mail.WithSSLPort(true))
|
||||
}
|
||||
|
||||
options = append(options, mail.WithUsername(config.Config().Core.Mail.Username))
|
||||
options = append(options, mail.WithPassword(config.Config().Core.Mail.Password))
|
||||
|
||||
client, err := mail.NewClient(config.Config().Core.Mail.Host, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.client = client
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
return m, nil
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package mailer
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const EMAIL_FS_PREFIX = "templates/"
|
||||
|
||||
const TPL_PASSWORD_RESET = "password_reset"
|
||||
const TPL_VERIFY_EMAIL = "verify_email"
|
||||
|
||||
type EmailTemplate struct {
|
||||
Subject *template.Template
|
||||
Body *template.Template
|
||||
}
|
||||
|
||||
//go:embed templates/*
|
||||
var templateFS embed.FS
|
||||
|
||||
var ErrTemplateNotFound = errors.New("template not found")
|
||||
|
||||
type TemplateRegistry struct {
|
||||
templates map[string]EmailTemplate
|
||||
}
|
||||
|
||||
func NewTemplateRegistry() *TemplateRegistry {
|
||||
return &TemplateRegistry{
|
||||
templates: make(map[string]EmailTemplate),
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *TemplateRegistry) loadTemplates() error {
|
||||
subjectTemplates, err := fs.Glob(templateFS, EMAIL_FS_PREFIX+"*_subject*")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, subjectTemplate := range subjectTemplates {
|
||||
templateName := strings.TrimPrefix(subjectTemplate, EMAIL_FS_PREFIX)
|
||||
templateName = strings.TrimSuffix(templateName, "_subject.tpl")
|
||||
bodyTemplate := strings.TrimSuffix(subjectTemplate, "_subject.tpl") + "_body.tpl"
|
||||
bodyTemplate = strings.TrimPrefix(bodyTemplate, EMAIL_FS_PREFIX)
|
||||
|
||||
subjectContent, err := fs.ReadFile(templateFS, EMAIL_FS_PREFIX+templateName+"_subject.tpl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subjectTmpl, err := template.New(templateName).Parse(string(subjectContent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bodyContent, err := fs.ReadFile(templateFS, EMAIL_FS_PREFIX+bodyTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bodyTmpl, err := template.New(templateName).Parse(string(bodyContent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tr.templates[templateName] = EmailTemplate{
|
||||
Subject: subjectTmpl,
|
||||
Body: bodyTmpl,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tr *TemplateRegistry) RenderTemplate(templateName string, subjectVars TemplateData, bodyVars TemplateData) (*Email, error) {
|
||||
tmpl, ok := tr.templates[templateName]
|
||||
if !ok {
|
||||
return nil, ErrTemplateNotFound
|
||||
}
|
||||
|
||||
var subjectBuilder strings.Builder
|
||||
err := tmpl.Subject.Execute(&subjectBuilder, subjectVars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var bodyBuilder strings.Builder
|
||||
err = tmpl.Body.Execute(&bodyBuilder, bodyVars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewEmail(subjectBuilder.String(), bodyBuilder.String()), nil
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
Dear {{if .FirstName}}{{.FirstName}}{{else}}{{.Email}}{{end}},
|
||||
|
||||
You are receiving this email because we received a password reset request for your account. If you did not request a password reset, please ignore this email.
|
||||
|
||||
To reset your password, please click the link below:
|
||||
{{.ResetURL}}
|
||||
|
||||
This link will expire in {{.ExpireTime}} hours. If you did not request a password reset, no further action is required.
|
||||
|
||||
If you're having trouble clicking the reset link, copy and paste the URL below into your web browser:
|
||||
{{.ResetURL}}
|
||||
|
||||
Thank you for using {{.PortalName}}.
|
||||
|
||||
Best regards,
|
||||
The {{.PortalName}} Team
|
|
@ -0,0 +1 @@
|
|||
Reset Your Password for {{.PortalName}}
|
|
@ -0,0 +1,10 @@
|
|||
Dear {{if .FirstName}}{{.FirstName}}{{else}}{{.Email}}{{end}},
|
||||
|
||||
Thank you for registering with {{.PortalName}}. To complete your registration and verify your email address, please go to the following link:
|
||||
|
||||
{{.VerificationLink}}
|
||||
|
||||
Please note, this link will expire in {{.ExpireTime}}. If you did not initiate this request, please ignore this email or contact our support team for assistance.
|
||||
|
||||
Best regards,
|
||||
The {{.PortalName}} Team
|
|
@ -0,0 +1 @@
|
|||
Verify Your Email for {{.PortalName}}
|
106
main.go
106
main.go
|
@ -1,106 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"git.lumeweb.com/LumeWeb/portal/config"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
_ "git.lumeweb.com/LumeWeb/portal/docs"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/files"
|
||||
"git.lumeweb.com/LumeWeb/portal/tus"
|
||||
"github.com/iris-contrib/swagger"
|
||||
"github.com/iris-contrib/swagger/swaggerFiles"
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
"go.uber.org/zap"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Embed a directory of static files for serving from the app's root path
|
||||
//
|
||||
//go:embed app/*
|
||||
var embedFrontend embed.FS
|
||||
|
||||
// @title Lume Web Portal
|
||||
// @version 1.0
|
||||
// @description A decentralized data storage portal for the open web
|
||||
|
||||
// @contact.name Lume Web Project
|
||||
// @contact.url https://lumeweb.com
|
||||
// @contact.email contact@lumeweb.com
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/license/mit/
|
||||
|
||||
// @externalDocs.description OpenAPI
|
||||
// @externalDocs.url https://swagger.io/resources/open-api/
|
||||
func main() {
|
||||
// Initialize the configuration settings
|
||||
config.Init()
|
||||
|
||||
// Initialize the database connection
|
||||
db.Init()
|
||||
logger.Init()
|
||||
files.Init()
|
||||
auth.Init()
|
||||
|
||||
// Create a new Iris app instance
|
||||
app := iris.New()
|
||||
// Enable Gzip compression for responses
|
||||
app.Use(iris.Compression)
|
||||
|
||||
// Serve static files from the embedded directory at the app's root path
|
||||
app.HandleDir("/", embedFrontend)
|
||||
|
||||
api := app.Party("/api")
|
||||
v1 := api.Party("/v1")
|
||||
|
||||
// Register the AccountController with the MVC framework and attach it to the "/api/account" path
|
||||
mvc.Configure(v1.Party("/account"), func(app *mvc.Application) {
|
||||
app.Handle(new(controller.AccountController))
|
||||
})
|
||||
|
||||
mvc.Configure(v1.Party("/auth"), func(app *mvc.Application) {
|
||||
app.Handle(new(controller.AuthController))
|
||||
})
|
||||
|
||||
mvc.Configure(v1.Party("/files"), func(app *mvc.Application) {
|
||||
app.Handle(new(controller.FilesController))
|
||||
app.Router.Use()
|
||||
})
|
||||
|
||||
tusHandler := tus.Init()
|
||||
|
||||
v1.Any(tus.TUS_API_PATH+"/{fileparam:path}", iris.FromStd(http.StripPrefix(v1.GetRelPath()+tus.TUS_API_PATH+"/", tusHandler)))
|
||||
v1.Post(tus.TUS_API_PATH, iris.FromStd(http.StripPrefix(v1.GetRelPath()+tus.TUS_API_PATH, tusHandler)))
|
||||
|
||||
swaggerConfig := swagger.Config{
|
||||
// The url pointing to API definition.
|
||||
URL: "http://localhost:8080/swagger/doc.json",
|
||||
DeepLinking: true,
|
||||
DocExpansion: "list",
|
||||
DomID: "#swagger-ui",
|
||||
// The UI prefix URL (see route).
|
||||
Prefix: "/swagger",
|
||||
}
|
||||
swaggerUI := swagger.Handler(swaggerFiles.Handler, swaggerConfig)
|
||||
|
||||
app.Get("/swagger", swaggerUI)
|
||||
// And the wildcard one for index.html, *.js, *.css and e.t.c.
|
||||
app.Get("/swagger/{any:path}", swaggerUI)
|
||||
|
||||
// Start the Iris app and listen for incoming requests on port 80
|
||||
err := app.Listen(":8080", func(app *iris.Application) {
|
||||
routes := app.GetRoutes()
|
||||
for _, route := range routes {
|
||||
log.Println(route)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error("Failed starting webserver proof", zap.Error(err))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.lumeweb.com/LumeWeb/portal/db/models"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var ErrNotFound = gorm.ErrRecordNotFound
|
||||
|
||||
var _ MetadataService = (*MetadataServiceDefault)(nil)
|
||||
|
||||
type UploadMetadata struct {
|
||||
ID uint `json:"upload_id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Hash []byte `json:"hash"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Protocol string `json:"protocol"`
|
||||
UploaderIP string `json:"uploader_ip"`
|
||||
Size uint64 `json:"size"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
func (u UploadMetadata) IsEmpty() bool {
|
||||
if u.UserID != 0 || u.MimeType != "" || u.Protocol != "" || u.UploaderIP != "" || u.Size != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !u.Created.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(u.Hash) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var Module = fx.Module("metadata",
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
NewMetadataService,
|
||||
fx.As(new(MetadataService)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
type MetadataService interface {
|
||||
SaveUpload(ctx context.Context, metadata UploadMetadata, skipExisting bool) error
|
||||
GetUpload(ctx context.Context, objectHash []byte) (UploadMetadata, error)
|
||||
DeleteUpload(ctx context.Context, objectHash []byte) error
|
||||
}
|
||||
|
||||
type MetadataServiceDefault struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type MetadataServiceParams struct {
|
||||
fx.In
|
||||
Db *gorm.DB
|
||||
}
|
||||
|
||||
func NewMetadataService(params MetadataServiceParams) *MetadataServiceDefault {
|
||||
return &MetadataServiceDefault{
|
||||
db: params.Db,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MetadataServiceDefault) SaveUpload(ctx context.Context, metadata UploadMetadata, skipExisting bool) error {
|
||||
var upload models.Upload
|
||||
|
||||
upload.Hash = metadata.Hash
|
||||
|
||||
ret := m.db.WithContext(ctx).Model(&models.Upload{}).Where(&upload).First(&upload)
|
||||
|
||||
if ret.Error != nil {
|
||||
if errors.Is(ret.Error, gorm.ErrRecordNotFound) {
|
||||
return m.createUpload(ctx, metadata)
|
||||
}
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
if skipExisting {
|
||||
return nil
|
||||
}
|
||||
|
||||
changed := false
|
||||
|
||||
if upload.UserID != metadata.UserID {
|
||||
upload.UserID = metadata.UserID
|
||||
changed = true
|
||||
}
|
||||
|
||||
if upload.MimeType != metadata.MimeType {
|
||||
upload.MimeType = metadata.MimeType
|
||||
changed = true
|
||||
}
|
||||
|
||||
if upload.Protocol != metadata.Protocol {
|
||||
upload.Protocol = metadata.Protocol
|
||||
changed = true
|
||||
}
|
||||
|
||||
if upload.UploaderIP != metadata.UploaderIP {
|
||||
upload.UploaderIP = metadata.UploaderIP
|
||||
changed = true
|
||||
}
|
||||
|
||||
if upload.Size != metadata.Size {
|
||||
upload.Size = metadata.Size
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
return m.db.Updates(&upload).Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MetadataServiceDefault) createUpload(ctx context.Context, metadata UploadMetadata) error {
|
||||
upload := models.Upload{
|
||||
UserID: metadata.UserID,
|
||||
Hash: metadata.Hash,
|
||||
MimeType: metadata.MimeType,
|
||||
Protocol: metadata.Protocol,
|
||||
UploaderIP: metadata.UploaderIP,
|
||||
Size: metadata.Size,
|
||||
}
|
||||
|
||||
return m.db.WithContext(ctx).Create(&upload).Error
|
||||
}
|
||||
|
||||
func (m *MetadataServiceDefault) GetUpload(ctx context.Context, objectHash []byte) (UploadMetadata, error) {
|
||||
var upload models.Upload
|
||||
|
||||
upload.Hash = objectHash
|
||||
|
||||
ret := m.db.WithContext(ctx).Model(&models.Upload{}).Where(&upload).First(&upload)
|
||||
|
||||
if ret.Error != nil {
|
||||
return UploadMetadata{}, ret.Error
|
||||
}
|
||||
|
||||
return UploadMetadata{
|
||||
ID: upload.ID,
|
||||
UserID: upload.UserID,
|
||||
Hash: upload.Hash,
|
||||
MimeType: upload.MimeType,
|
||||
Protocol: upload.Protocol,
|
||||
UploaderIP: upload.UploaderIP,
|
||||
Size: upload.Size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MetadataServiceDefault) DeleteUpload(ctx context.Context, objectHash []byte) error {
|
||||
var upload models.Upload
|
||||
|
||||
upload.Hash = objectHash
|
||||
|
||||
ret := m.db.WithContext(ctx).Model(&models.Upload{}).Where(&upload).First(&upload)
|
||||
|
||||
if ret.Error != nil {
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
return m.db.Delete(&upload).Error
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue