diff --git a/Pipfile b/Pipfile index 44e04f14ff..1798dee31b 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,9 @@ typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +bcrypt = "*" +flask-mail = "*" +tomli = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index b201c3decc..90da17a02c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5" + "sha256": "c058e688b7411227f2fb46d5d4a65058971e20b2dae6c2c384acf03a9953dfc0" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,69 @@ "default": { "alembic": { "hashes": [ - "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", - "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" + "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d", + "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2" ], + "markers": "python_version >= '3.9'", + "version": "==1.16.4" + }, + "bcrypt": { + "hashes": [ + "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", + "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", + "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", + "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", + "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", + "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", + "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", + "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", + "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", + "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", + "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", + "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", + "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", + "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", + "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", + "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", + "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", + "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", + "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", + "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", + "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", + "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", + "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", + "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", + "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", + "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", + "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", + "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", + "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", + "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", + "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", + "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", + "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", + "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", + "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", + "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", + "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", + "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", + "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", + "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", + "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", + "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", + "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", + "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", + "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", + "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", + "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", + "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", + "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", + "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", + "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d" + ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.14.1" + "version": "==4.3.0" }, "blinker": { "hashes": [ @@ -34,35 +92,36 @@ }, "certifi": { "hashes": [ - "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", - "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" + "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", + "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5" ], - "markers": "python_version >= '3.6'", - "version": "==2025.1.31" + "markers": "python_version >= '3.7'", + "version": "==2025.8.3" }, "click": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", + "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "markers": "python_version >= '3.10'", + "version": "==8.2.1" }, "cloudinary": { "hashes": [ - "sha256:ba223705409b2aaddd5196c2184d65f50a83dffcba3b94f3727658ff6a0172a3", - "sha256:e4191b470c5bae55542b64e0a78659af42971880294456dca480bc974fa9280a" + "sha256:62d4374b79d5476de2a86cb6a1da709a5429e02aef474bfc5d99f3e38a1a62ff", + "sha256:b4785031179a5ec7010f46665e5c8fad2cae022c18405546f01d257e02f78b1c" ], "index": "pypi", - "version": "==1.42.2" + "version": "==1.44.1" }, "flask": { "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" + "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", + "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c" ], "index": "pypi", - "version": "==3.1.0" + "markers": "python_version >= '3.9'", + "version": "==3.1.2" }, "flask-admin": { "hashes": [ @@ -70,15 +129,17 @@ "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==1.6.1" }, "flask-cors": { "hashes": [ - "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", - "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c" + "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", + "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db" ], "index": "pypi", - "version": "==5.0.1" + "markers": "python_version >= '3.9' and python_version < '4.0'", + "version": "==6.0.1" }, "flask-jwt-extended": { "hashes": [ @@ -86,14 +147,25 @@ "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2" ], "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", "version": "==4.6.0" }, + "flask-mail": { + "hashes": [ + "sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d", + "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.10.0" + }, "flask-migrate": { "hashes": [ "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==4.1.0" }, "flask-sqlalchemy": { @@ -102,6 +174,7 @@ "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.1.1" }, "flask-swagger": { @@ -114,82 +187,63 @@ }, "greenlet": { "hashes": [ - "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", - "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", - "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", - "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", - "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", - "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", - "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", - "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", - "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", - "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa", - "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", - "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", - "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", - "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", - "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", - "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", - "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", - "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", - "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", - "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", - "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291", - "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", - "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", - "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", - "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", - "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef", - "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", - "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", - "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", - "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", - "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", - "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", - "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", - "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", - "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", - "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", - "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", - "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", - "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", - "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", - "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", - "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", - "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", - "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", - "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", - "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", - "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981", - "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", - "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", - "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798", - "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", - "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", - "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", - "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", - "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af", - "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", - "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", - "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", - "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", - "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", - "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", - "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", - "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc", - "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de", - "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", - "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", - "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", - "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", - "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", - "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", - "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803", - "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", - "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" - ], - "markers": "python_version < '3.14' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))))", - "version": "==3.1.1" + "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", + "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", + "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", + "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", + "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", + "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", + "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", + "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", + "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", + "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", + "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", + "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", + "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", + "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", + "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", + "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", + "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", + "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", + "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", + "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", + "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", + "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", + "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", + "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", + "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", + "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", + "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", + "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", + "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", + "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", + "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", + "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", + "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", + "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", + "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", + "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", + "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", + "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", + "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", + "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", + "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", + "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", + "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", + "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", + "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", + "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", + "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", + "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", + "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", + "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", + "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", + "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", + "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", + "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968" + ], + "markers": "python_version >= '3.9'", + "version": "==3.2.4" }, "gunicorn": { "hashes": [ @@ -197,6 +251,7 @@ "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==23.0.0" }, "itsdangerous": { @@ -209,19 +264,19 @@ }, "jinja2": { "hashes": [ - "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", - "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], "markers": "python_version >= '3.7'", - "version": "==3.1.5" + "version": "==3.1.6" }, "mako": { "hashes": [ - "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", - "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" + "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", + "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" ], "markers": "python_version >= '3.8'", - "version": "==1.3.9" + "version": "==1.3.10" }, "markupsafe": { "hashes": [ @@ -292,11 +347,11 @@ }, "packaging": { "hashes": [ - "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", - "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" ], "markers": "python_version >= '3.8'", - "version": "==24.2" + "version": "==25.0" }, "psycopg2-binary": { "hashes": [ @@ -370,6 +425,7 @@ "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==2.9.10" }, "pyjwt": { @@ -382,11 +438,12 @@ }, "python-dotenv": { "hashes": [ - "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", - "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", + "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab" ], "index": "pypi", - "version": "==1.0.1" + "markers": "python_version >= '3.9'", + "version": "==1.1.1" }, "pyyaml": { "hashes": [ @@ -457,82 +514,123 @@ }, "sqlalchemy": { "hashes": [ - "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d", - "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03", - "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea", - "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50", - "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d", - "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3", - "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1", - "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727", - "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68", - "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149", - "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06", - "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7", - "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca", - "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5", - "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3", - "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3", - "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443", - "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff", - "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86", - "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6", - "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753", - "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2", - "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297", - "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578", - "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728", - "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178", - "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2", - "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096", - "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9", - "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8", - "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b", - "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4", - "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a", - "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079", - "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725", - "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373", - "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248", - "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd", - "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda", - "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6", - "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579", - "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444", - "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d", - "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4", - "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc", - "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7", - "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c", - "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba", - "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32", - "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e", - "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb", - "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120", - "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd", - "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e", - "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63", - "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2", - "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae" + "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019", + "sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7", + "sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c", + "sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227", + "sha256:11f43c39b4b2ec755573952bbcc58d976779d482f6f832d7f33a8d869ae891bf", + "sha256:13194276e69bb2af56198fef7909d48fd34820de01d9c92711a5fa45497cc7ed", + "sha256:136063a68644eca9339d02e6693932116f6a8591ac013b0014479a1de664e40a", + "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", + "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", + "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", + "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", + "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", + "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", + "sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed", + "sha256:227119ce0a89e762ecd882dc661e0aa677a690c914e358f0dd8932a2e8b2765b", + "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", + "sha256:334f41fa28de9f9be4b78445e68530da3c5fa054c907176460c81494f4ae1f5e", + "sha256:413391b2239db55be14fa4223034d7e13325a1812c8396ecd4f2c08696d5ccad", + "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", + "sha256:44337823462291f17f994d64282a71c51d738fc9ef561bf265f1d0fd9116a782", + "sha256:46293c39252f93ea0910aababa8752ad628bcce3a10d3f260648dd472256983f", + "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b", + "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3", + "sha256:4e6aeb2e0932f32950cf56a8b4813cb15ff792fc0c9b3752eaf067cfe298496a", + "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", + "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", + "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", + "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921", + "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", + "sha256:61f964a05356f4bca4112e6334ed7c208174511bd56e6b8fc86dad4d024d4185", + "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", + "sha256:6e2bf13d9256398d037fef09fd8bf9b0bf77876e22647d10761d35593b9ac547", + "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069", + "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", + "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", + "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154", + "sha256:8cee08f15d9e238ede42e9bbc1d6e7158d0ca4f176e4eab21f88ac819ae3bd7b", + "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", + "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18", + "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", + "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", + "sha256:b3edaec7e8b6dc5cd94523c6df4f294014df67097c8217a89929c99975811414", + "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", + "sha256:bcf0724a62a5670e5718957e05c56ec2d6850267ea859f8ad2481838f889b42c", + "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612", + "sha256:c379e37b08c6c527181a397212346be39319fb64323741d23e46abd97a400d34", + "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8", + "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20", + "sha256:c697575d0e2b0a5f0433f679bda22f63873821d991e95a90e9e52aae517b2e32", + "sha256:cdeff998cb294896a34e5b2f00e383e7c5c4ef3b4bfa375d9104723f15186443", + "sha256:ceb5c832cc30663aeaf5e39657712f4c4241ad1f638d487ef7216258f6d41fe7", + "sha256:d34c0f6dbefd2e816e8f341d0df7d4763d382e3f452423e752ffd1e213da2512", + "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca", + "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00", + "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", + "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", + "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d" ], "index": "pypi", - "version": "==2.0.38" + "markers": "python_version >= '3.7'", + "version": "==2.0.43" + }, + "tomli": { + "hashes": [ + "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", + "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", + "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", + "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", + "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", + "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", + "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", + "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", + "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", + "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", + "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", + "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", + "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", + "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", + "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", + "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", + "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", + "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", + "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", + "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", + "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", + "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", + "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", + "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", + "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", + "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", + "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", + "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", + "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", + "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", + "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", + "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.2.1" }, "typing-extensions": { "hashes": [ - "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", + "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76" ], "index": "pypi", - "version": "==4.12.2" + "markers": "python_version >= '3.9'", + "version": "==4.14.1" }, "urllib3": { "hashes": [ - "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", - "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" ], "markers": "python_version >= '3.9'", - "version": "==2.3.0" + "version": "==2.5.0" }, "werkzeug": { "hashes": [ @@ -548,6 +646,7 @@ "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.1.2" } }, diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py deleted file mode 100644 index 88964176f1..0000000000 --- a/migrations/versions/0763d677d453_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 0763d677d453 -Revises: -Create Date: 2025-02-25 14:47:16.337069 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0763d677d453' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user') - # ### end Alembic commands ### diff --git a/migrations/versions/91adadb09156_.py b/migrations/versions/91adadb09156_.py new file mode 100644 index 0000000000..5b89eff783 --- /dev/null +++ b/migrations/versions/91adadb09156_.py @@ -0,0 +1,58 @@ +"""empty message + +Revision ID: 91adadb09156 +Revises: +Create Date: 2025-08-22 06:46:36.007477 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '91adadb09156' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password', sa.String(), nullable=False), + sa.Column('vehicle', sa.Boolean(), nullable=False), + sa.Column('coordenates', sa.String(length=120), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('vehicle_consume_km', sa.Float(precision=50), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('coordenates'), + sa.UniqueConstraint('email') + ) + op.create_table('oferta', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('id_comprador', sa.Integer(), nullable=True), + sa.Column('id_vendedor', sa.Integer(), nullable=False), + sa.Column('esta_realizada', sa.Boolean(), nullable=False), + sa.Column('descripcion', sa.String(length=600), nullable=False), + sa.Column('titulo', sa.String(length=200), nullable=False), + sa.Column('coordenates_vendedor', sa.String(length=120), nullable=False), + sa.Column('coordenates_comprador', sa.String(length=120), nullable=True), + sa.Column('precio_ud', sa.Integer(), nullable=True), + sa.Column('ud', sa.String(length=200), nullable=False), + sa.Column('img_cosecha', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['coordenates_comprador'], ['user.coordenates'], ), + sa.ForeignKeyConstraint(['coordenates_vendedor'], ['user.coordenates'], ), + sa.ForeignKeyConstraint(['id_comprador'], ['user.id'], ), + sa.ForeignKeyConstraint(['id_vendedor'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('oferta') + op.drop_table('user') + # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..e0d5a6b9e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,11 @@ "version": "1.0.1", "license": "ISC", "dependencies": { + "@vis.gl/react-google-maps": "^1.5.4", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.30.1" }, "devDependencies": { "@types/react": "^18.2.18", @@ -944,9 +945,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", - "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -997,6 +998,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "16.11.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", @@ -1040,6 +1047,20 @@ "dev": true, "license": "ISC" }, + "node_modules/@vis.gl/react-google-maps": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.5.4.tgz", + "integrity": "sha512-pD3e2wDtOfd439mamkacRgrM6I2B/lue61QCR0pGQT8MVaG9pz9/LajHbsjZW2lms8Ao8mf2PQJeiGC2FxI0Fw==", + "license": "MIT", + "dependencies": { + "@types/google.maps": "^3.54.10", + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "react": ">=16.8.0 || ^19.0 || ^19.0.0-rc", + "react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -2215,8 +2236,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -3522,12 +3542,12 @@ } }, "node_modules/react-router": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz", - "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.22.0" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -3537,13 +3557,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz", - "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.22.0", - "react-router": "6.29.0" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { "node": ">=14.0.0" @@ -4999,9 +5019,9 @@ } }, "@remix-run/router": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", - "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==" + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==" }, "@types/babel__core": { "version": "7.20.5", @@ -5044,6 +5064,11 @@ "@babel/types": "^7.20.7" } }, + "@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==" + }, "@types/node": { "version": "16.11.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", @@ -5081,6 +5106,15 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "@vis.gl/react-google-maps": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.5.4.tgz", + "integrity": "sha512-pD3e2wDtOfd439mamkacRgrM6I2B/lue61QCR0pGQT8MVaG9pz9/LajHbsjZW2lms8Ao8mf2PQJeiGC2FxI0Fw==", + "requires": { + "@types/google.maps": "^3.54.10", + "fast-deep-equal": "^3.1.3" + } + }, "@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -5883,8 +5917,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { "version": "2.1.0", @@ -6727,20 +6760,20 @@ "dev": true }, "react-router": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz", - "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "requires": { - "@remix-run/router": "1.22.0" + "@remix-run/router": "1.23.0" } }, "react-router-dom": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz", - "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "requires": { - "@remix-run/router": "1.22.0", - "react-router": "6.29.0" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" } }, "reflect.getprototypeof": { diff --git a/package.json b/package.json index 0caab10749..f5879dc9d3 100755 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "main": "index.js", "scripts": { "dev": "vite", - "start": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "start": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" }, "author": { "name": "Alejandro Sanchez", @@ -30,13 +30,13 @@ "license": "ISC", "devDependencies": { "@types/react": "^18.2.18", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.4", - "eslint": "^8.46.0", - "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.8" + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.4", + "eslint": "^8.46.0", + "eslint-plugin-react": "^7.33.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "vite": "^4.4.8" }, "babel": { "presets": [ @@ -54,9 +54,10 @@ ] }, "dependencies": { + "@vis.gl/react-google-maps": "^1.5.4", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.30.1" } } diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..351b59809b 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,64 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean +from sqlalchemy import String, Boolean, Float,Integer,ForeignKey from sqlalchemy.orm import Mapped, mapped_column db = SQLAlchemy() class User(db.Model): + __tablename__ = "user" + id: Mapped[int] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + vehicle: Mapped[bool] = mapped_column(Boolean(), nullable=False) + coordenates: Mapped[str] = mapped_column(String(120),nullable=False, unique=True) + name: Mapped[str]= mapped_column(String(200),nullable=False) + vehicle_consume_km: Mapped[float] = mapped_column(Float(50),nullable= True) + def serialize(self): return { "id": self.id, "email": self.email, + "name":self.name, + "coordenates":self.coordenates, + "vehicle":self.vehicle, + "vehicle_consume_km":self.vehicle_consume_km + + # do not serialize the password, its a security breach + } + +class Oferta(db.Model): + __tablename__="oferta" + + id: Mapped[int] = mapped_column(primary_key=True) + id_comprador: Mapped[int] = mapped_column(Integer(),ForeignKey("user.id"), nullable=True) + id_vendedor: Mapped[int] = mapped_column(Integer(),ForeignKey("user.id"),nullable=False) + esta_realizada: Mapped[bool] = mapped_column(Boolean(), nullable=False) + descripcion: Mapped[str] = mapped_column(String(600),nullable=False) + titulo: Mapped[str]= mapped_column(String(200),nullable=False) + coordenates_vendedor: Mapped[str] = mapped_column(String(120),ForeignKey("user.coordenates"),nullable=False) + coordenates_comprador: Mapped[str] = mapped_column(String(120),ForeignKey("user.coordenates"),nullable=True) + precio_ud: Mapped[int] = mapped_column(Integer(),nullable=True) + ud:Mapped[str] = mapped_column(String(200),nullable=False) + img_cosecha:Mapped[str] = mapped_column(String(),nullable=True) + + + + + def serialize(self): + return { + "id": self.id, + "id_comprador": self.id_comprador, + "id_vendedor":self.id_vendedor, + "esta_realizada":self.esta_realizada, + "descripcion":self.descripcion, + "titulo":self.titulo, + "coordenates_vendedor":self.coordenates_vendedor, + "coordenates_comprador":self.coordenates_comprador, + "precio_ud":self.precio_ud, + "img_cosecha":self.img_cosecha + # do not serialize the password, its a security breach } \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..09dcb313d8 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,10 +1,27 @@ """ This module takes care of starting the API Server, Loading the DB and Adding the endpoints """ + from flask import Flask, request, jsonify, url_for, Blueprint -from api.models import db, User +from api.models import db, User, Oferta from api.utils import generate_sitemap, APIException from flask_cors import CORS +import bcrypt +from flask_jwt_extended import create_access_token +from flask_jwt_extended import jwt_required, get_jwt_identity +from flask_mail import Message +from extension import mail +import os +import re +from flask_jwt_extended import decode_token +import jwt + + + +url_front = os.getenv('VITE_FRONT_URL') + + + api = Blueprint('api', __name__) @@ -12,7 +29,9 @@ CORS(api) -@api.route('/hello', methods=['POST', 'GET']) + + +@api.route('/', methods=['POST', 'GET']) def handle_hello(): response_body = { @@ -20,3 +39,196 @@ def handle_hello(): } return jsonify(response_body), 200 + +# Post para registrar un usuario +@api.route('/user/register', methods=['POST']) +def user_register(): + + body = request.get_json() + new_pass=bcrypt.hashpw(body["password"].encode(), bcrypt.gensalt()) + + + new_user = User() + new_user.name = body["name"] + new_user.email = body["email"] + new_user.password = new_pass.decode() + new_user.vehicle = body["vehicle"] + new_user.vehicle_consume_km = body["vehicle_consume_km"] + new_user.coordenates = body["coordenates"] + + db.session.add(new_user) + db.session.commit() + + return jsonify("new_user"), 200 + + + +@api.route('/user/resetPassword', methods=['PUT']) +def user_resetPassWord(): + body = request.get_json() + token = body.get("token") + if not token: + return jsonify({"error": "No tengo ni Token ni mierda en las tripas"}), 401 + try: + decoded_token = decode_token(token) + user_id = decoded_token["sub"] + user = User.query.filter_by(id=int(user_id)).first + if not user: + return jsonify({"error": "Usuario no valido esto es una verga"}), 400 + password_data = body.get("password") + if password_data: + new_password = password_data.get("nuevaContraseña") + else: + new_password = body.get("password") + if not new_password: + return jsonify({"error": "Nueva contraseña media chota"}), 400 + hashed_password = bcrypt.hashpw( + new_password.encode(), bcrypt.gensalt()) + user.password = hashed_password.decode() + db.session.commit() + return jsonify({"msg": "Contaseña se ha actualizado ya no es la vieja es la nueva weon"}) + except Exception as e: + db.session.rollback() + print(f"error: {e}") + return jsonify({"error": "Error al actualizar la contraseña checkea la movie del codigo"}) + +# Post para logear un usuario +@api.route("/user/login", methods=["POST"]) +def user_login(): + body = request.get_json() + user = User.query.filter_by(email=body["email"]).first() + user_pass = User.query.filter_by(password=body["password"]).first() + + if user is None: + return jsonify("Cuenta no existe"),404 + + if bcrypt.checkpw(body["password"].encode(),user.password.encode()): + user_serialize = user.serialize() + token = create_access_token(identity = str(user_serialize["id"])) + return jsonify({"token":token},{"user":user_serialize}),200 + + + return jsonify("Usuario logueado"),200 + +# GET pedir informacion sobre un usuario +@api.route("/user", methods=["GET"]) +@jwt_required() +def get_user(): + current_user = get_jwt_identity() + user = User.query.get(current_user) + if user is None: + return jsonify("Usuario no valido"),400 + return jsonify({"user":user.serialize()}) + + +# GET pedir informacion sobre todas las ofertas disponibles de todos los usuarios +@api.route("/user/ofertas", methods=["GET"]) + +def get_ofertas(): + + ofertas = Oferta.query.all() + iterar_ofertas = [oferta.serialize() for oferta in ofertas] + if ofertas is None: + return jsonify("No hay ofertas disponibles"),404 + return jsonify({"ofertas": iterar_ofertas}),200 + +# GET pedir informacion sobre una oferta + +@api.route("/user/oferta/info/", methods=["GET"]) +@jwt_required() +def get_oferta(oferta_id): + current_user = get_jwt_identity() + user = User.query.get(current_user) + if user is None: + return jsonify("Usuario no valido"),400 + + oferta = Oferta.query.get(oferta_id) + + if oferta is None: + return jsonify("No existe esa oferta"),400 + oferta_serializada = oferta.serialize() + print(oferta_serializada) + print(oferta) + print(oferta_id) + + return jsonify(oferta_serializada) + + +# POST crear una nueva oferta +@api.route("/user/ofertas", methods=["POST"]) +@jwt_required() +def post_ofertas(): + current_user = get_jwt_identity() + user = User.query.get(current_user) + + body = request.get_json() + nueva_oferta = Oferta() + nueva_oferta.id_comprador = None + nueva_oferta.coordenates_comprador = None + nueva_oferta.id_vendedor = user.id + nueva_oferta.esta_realizada = False + nueva_oferta.descripcion = body["descripcion"] + nueva_oferta.titulo = body["titulo"] + nueva_oferta.coordenates_vendedor = user.coordenates + nueva_oferta.precio_ud = body["precio_ud"] + nueva_oferta.ud = body["ud"] + nueva_oferta.img_cosecha = body["img_cosecha"] + + db.session.add(nueva_oferta) + db.session.commit() + + + if user is None: + return jsonify("Usuario no valido"),400 + return jsonify(nueva_oferta.serialize()),200 + + +# PUT comprar una oferta + +@api.route("/user/oferta/comprar/", methods=["PUT"]) +@jwt_required() +def comprar_oferta(oferta_id): + current_user = get_jwt_identity() + user = User.query.get(current_user) + if user is None: + return jsonify("Usuario no valido"),400 + + oferta = Oferta.query.get(oferta_id) + oferta.id_comprador = user.id + oferta.coordenates_comprador = user.coordenates + oferta.esta_realizada = True + db.session.add(oferta) + db.session.commit() + + + if oferta is None: + return jsonify("No existe esa oferta"),400 + oferta_serializada = oferta.serialize() + print(oferta_serializada) + print(oferta) + print(oferta_id) + + return jsonify(oferta_serializada) + + + +@api.route("/resetPassword", methods=['POST']) +def resetPassword(): + data = request.get_json() + user_email = data.get('email') + user = User.query.filter_by(email=data["email"]).first() + user_serialize = user.serialize() + token = create_access_token(identity=str(user_serialize["id"])) + cadena_modificada = re.sub(r"\.", "_", token) + reset_url_password = f"{url_front}resetPassword/{cadena_modificada}" + + msg = Message( + "Email", + html=f"

para restablecer la contraseña, da click aqui

", + sender="u7384442007@gmail.com", + recipients=[user_email], + ) + + mail.send(msg) + + return jsonify("Enviado co"),200 \ No newline at end of file diff --git a/src/app.py b/src/app.py index 1b3340c0fa..c90572990e 100644 --- a/src/app.py +++ b/src/app.py @@ -10,14 +10,20 @@ from api.routes import api from api.admin import setup_admin from api.commands import setup_commands - +from flask_jwt_extended import JWTManager +from flask import Flask +from flask_mail import Mail +from api import routes +from extension import mail # from models import Person + ENV = "development" if os.getenv("FLASK_DEBUG") == "1" else "production" static_file_dir = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../dist/') app = Flask(__name__) app.url_map.strict_slashes = False +mail.init_app(app) # database condiguration db_url = os.getenv("DATABASE_URL") @@ -28,9 +34,14 @@ app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:////tmp/test.db" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +app.config["JWT_SECRET_KEY"] = os.getenv("TOKEN_KEY") + MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) +jwt = JWTManager(app) + # add the admin setup_admin(app) @@ -42,6 +53,16 @@ # Handle/serialize errors like a JSON object +app.config['MAIL_SERVER'] = 'smtp.gmail.com' +app.config['MAIL_PORT'] = 587 +app.config['MAIL_USE_TLS'] = True +app.config['MAIL_USERNAME'] = 'u7384442007@gmail.com' +app.config['MAIL_PASSWORD'] = 'kilj rrzk ipsz nkis' +app.config['MAIL_DEFAULT_SENDER'] = 'u7384442007@gmail.com' + + # Inicializar extensiones +mail.init_app(app) + @app.errorhandler(APIException) def handle_invalid_usage(error): diff --git a/src/extension.py b/src/extension.py new file mode 100644 index 0000000000..3d1d121617 --- /dev/null +++ b/src/extension.py @@ -0,0 +1,3 @@ +from flask_mail import Mail + +mail = Mail() \ No newline at end of file diff --git a/src/front/AppRouter.jsx b/src/front/AppRouter.jsx new file mode 100644 index 0000000000..b6048c67a2 --- /dev/null +++ b/src/front/AppRouter.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { Login } from "./pages/Login"; +import { Register } from "./pages/Registro"; + +export const AppRouter = () => { + return ( + + + } /> + } /> + + + ); +}; \ No newline at end of file diff --git a/src/front/components/GoogleMapWithCustomControl.jsx b/src/front/components/GoogleMapWithCustomControl.jsx new file mode 100644 index 0000000000..374e4a6a2e --- /dev/null +++ b/src/front/components/GoogleMapWithCustomControl.jsx @@ -0,0 +1,59 @@ +import React, { useEffect, useRef, useState } from 'react'; +import useGlobalReducer from "../hooks/useGlobalReducer"; +import { APIProvider, useMap, Map } from '@vis.gl/react-google-maps'; +import {AdvancedMarker} from '@vis.gl/react-google-maps'; + +const MADRID_LOCATION = { lat: 40.4168, lng: -3.7038 }; +const logoSvgStyles = { + marginRight: '10px', + width: '35px', + height: '35px', + fill: '#ffeb3b', // Amarillo cereal +}; + + +export const GoogleMapWithCustomControl = () => { + + + const {store,dispatch} = useGlobalReducer() + const [coordenadas, setCoordenadas] = useState({ + latitude: MADRID_LOCATION.lat, + longitude: MADRID_LOCATION.lng + }); + + + + + + return ( + + { + const newCoordenates={ + latitude: e.detail.latLng?.lat || 0, + longitude: e.detail.latLng?.lng || 0,} + setCoordenadas(newCoordenates) + dispatch({ + type : "add_coordenates", + payload : newCoordenates + }) + + }} > + + + + + + + + ) +}; + diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..f580080ace 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,19 +1,142 @@ -import { Link } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import useGlobalReducer from '../hooks/useGlobalReducer'; + +// --- Estilos CSS en línea para la Navbar --- +const navbarContainerStyles = { + backgroundColor: '#8bc34a', // Verde oliva claro, evocando campos + padding: '15px 30px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + boxShadow: '0 4px 10px rgba(0, 70, 0, 0.2)', // Sombra sutil con tono verde + fontFamily: 'Arial, sans-serif', + borderRadius: '8px', // Bordes redondeados para un toque orgánico + margin: '20px auto', + maxWidth: '1200px', + boxSizing: 'border-box', +}; + +const logoStyles = { + display: 'flex', + alignItems: 'center', + textDecoration: 'none', // Elimina el subrayado del enlace + color: 'white', + fontWeight: 'bold', + fontSize: '1.8em', + textShadow: '1px 1px 3px rgba(0,0,0,0.2)', + letterSpacing: '0.5px', +}; + +const logoSvgStyles = { + marginRight: '10px', + width: '35px', + height: '35px', + fill: '#ffeb3b', // Amarillo cereal +}; + +const navLinksStyles = { + display: 'flex', + gap: '25px', +}; + +const linkStyles = { + color: 'white', + textDecoration: 'none', + fontSize: '1.1em', + padding: '8px 15px', + borderRadius: '5px', + transition: 'background-color 0.3s ease, transform 0.2s ease', + fontWeight: 'bold', + letterSpacing: '0.2px', +}; + +const linkHoverStyles = { + backgroundColor: '#689f38', // Verde más oscuro al pasar el ratón + transform: 'translateY(-2px)', +}; +const buttonStyles = { + ...linkStyles, // Inherit base link styles + border: 'none', + backgroundColor: 'transparent', // Make button background transparent by default + // Specific styles for hover/active states if needed for buttons +}; + +const buttonHoverStyles = { + backgroundColor: '#689f38', // Darker green on hover + transform: 'translateY(-2px)', +}; + +// --- Componente NavbarAgricola --- export const Navbar = () => { + const navigate = useNavigate(); + const { store, dispatch } = useGlobalReducer(); + + const handleLogout = () => { + + dispatch({ type: 'eliminar_user' }); // Dispatch the action + localStorage.removeItem('jwt_token'); + // 3. Redirect to login page + navigate('/registro'); + console.log('User logged out.'); // For debugging + }; + + return ( + + ); +}; - return ( - - ); -}; \ No newline at end of file +export default Navbar; \ No newline at end of file diff --git a/src/front/main.jsx b/src/front/main.jsx index a5a3c781dc..168d0e5f34 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,3 +1,4 @@ + import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' // Global styles for your application diff --git a/src/front/pages/BusquedaOfertas.jsx b/src/front/pages/BusquedaOfertas.jsx new file mode 100644 index 0000000000..8e8fb92fcd --- /dev/null +++ b/src/front/pages/BusquedaOfertas.jsx @@ -0,0 +1,594 @@ +import React, { useEffect, useState, useMemo } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import useGlobalReducer from '../hooks/useGlobalReducer'; +import { APIProvider, useMap, Map } from '@vis.gl/react-google-maps'; +import {AdvancedMarker} from '@vis.gl/react-google-maps'; +import { beautifulStyles } from "../styles/beautifulStyles"; + +export const BusquedaOfertas = () => { + const { store, dispatch } = useGlobalReducer(); + const [offers, setOffers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + const navigate = useNavigate(); + + // Estados para filtros y búsqueda + const [searchFilters, setSearchFilters] = useState({ + searchText: '', + sortBy: 'relevance', + priceRange: { min: '', max: '' }, + productType: 'all', + availableOnly: false, + userLocation: null + }); + + // Obtener ubicación del usuario para búsqueda por cercanía + useEffect(() => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + setSearchFilters(prev => ({ + ...prev, + userLocation: { + lat: position.coords.latitude, + lng: position.coords.longitude + } + })); + }, + (error) => { + console.log('Error getting user location:', error); + } + ); + } + }, []); + + useEffect(() => { + const fetchOffers = async () => { + try { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + if (!backendUrl) { + throw new Error("VITE_BACKEND_URL is not defined"); + } + + const res = await fetch(`${backendUrl}api/user/ofertas`); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const data = await res.json(); + setOffers(data.ofertas || []); + } catch (err) { + console.error('Error fetching offers:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchOffers(); + }, []); + + const isValidCoordinate = (num) => { + return !isNaN(num) && isFinite(num); + }; + + const getValidCoordinates = (coordenates_vendedor) => { + if (!coordenates_vendedor) return null; + + let lat, lng; + + if (typeof coordenates_vendedor === 'string') { + const cleaned = coordenates_vendedor.replace(/[{}]/g, ''); + const coords = cleaned.split(','); + + if (coords.length >= 2) { + lat = parseFloat(coords[0].trim()); + lng = parseFloat(coords[1].trim()); + } else { + return null; + } + } else if (typeof coordenates_vendedor === 'object' && !Array.isArray(coordenates_vendedor)) { + lat = parseFloat(coordenates_vendedor.lat || coordenates_vendedor.latitude); + lng = parseFloat(coordenates_vendedor.lng || coordenates_vendedor.longitude || coordenates_vendedor.lon); + } else if (Array.isArray(coordenates_vendedor) && coordenates_vendedor.length >= 2) { + lat = parseFloat(coordenates_vendedor[0]); + lng = parseFloat(coordenates_vendedor[1]); + } else { + return null; + } + + if (isValidCoordinate(lat) && isValidCoordinate(lng)) { + if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + return { lat, lng }; + } + } + + return null; + }; + + // Función para calcular distancia entre dos puntos (fórmula de Haversine) + const calculateDistance = (lat1, lng1, lat2, lng2) => { + const R = 6371; // Radio de la Tierra en km + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLng = (lng2 - lng1) * Math.PI / 180; + const a = + Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLng/2) * Math.sin(dLng/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; // Distancia en km + }; + + const getCropIcon = (title) => { + if (!title) return 🌱; + + const titleLower = title.toLowerCase(); + + const iconMap = { + 'trigo|cereal|avena': '🌾', + 'tomate|verdura|hortaliza': '🍅', + 'fruta|manzana|pera': '🍎', + 'uva|vino|viña': '🍇', + 'oliva|aceite|aceituna': '🫒', + 'naranja|limón|cítrico': '🍊', + 'girasol|flor': '🌻', + 'maíz|grano': '🌽', + 'lechuga|ensalada': '🥬', + 'zanahoria|tubérculo': '🥕' + }; + + for (const [keywords, icon] of Object.entries(iconMap)) { + if (keywords.split('|').some(keyword => titleLower.includes(keyword))) { + return {icon}; + } + } + + return 🌱; + }; + + // Función para determinar el tipo de producto + const getProductType = (title) => { + if (!title) return 'otros'; + + const titleLower = title.toLowerCase(); + + if (['trigo', 'cereal', 'avena', 'maíz', 'grano'].some(keyword => titleLower.includes(keyword))) { + return 'cereales'; + } + if (['fruta', 'manzana', 'pera', 'uva', 'naranja', 'limón', 'cítrico'].some(keyword => titleLower.includes(keyword))) { + return 'frutas'; + } + if (['tomate', 'verdura', 'hortaliza', 'lechuga', 'ensalada', 'zanahoria'].some(keyword => titleLower.includes(keyword))) { + return 'verduras'; + } + if (['oliva', 'aceite', 'aceituna', 'girasol'].some(keyword => titleLower.includes(keyword))) { + return 'aceites'; + } + + return 'otros'; + }; + + // Filtrar y ordenar ofertas usando useMemo para optimizar performance + const filteredAndSortedOffers = useMemo(() => { + let filtered = [...offers]; + + // Filtro por texto de búsqueda + if (searchFilters.searchText.trim()) { + const searchTerm = searchFilters.searchText.toLowerCase().trim(); + filtered = filtered.filter(offer => + offer.titulo?.toLowerCase().includes(searchTerm) || + offer.descripcion?.toLowerCase().includes(searchTerm) + ); + } + + // Filtro por tipo de producto + if (searchFilters.productType !== 'all') { + filtered = filtered.filter(offer => + getProductType(offer.titulo) === searchFilters.productType + ); + } + + // Filtro por rango de precios + if (searchFilters.priceRange.min) { + filtered = filtered.filter(offer => + parseFloat(offer.precio_ud) >= parseFloat(searchFilters.priceRange.min) + ); + } + if (searchFilters.priceRange.max) { + filtered = filtered.filter(offer => + parseFloat(offer.precio_ud) <= parseFloat(searchFilters.priceRange.max) + ); + } + + // Filtro por disponibilidad + if (searchFilters.availableOnly) { + filtered = filtered.filter(offer => !offer.esta_realizada); + } + + // Ordenamiento + filtered.sort((a, b) => { + switch (searchFilters.sortBy) { + case 'price-asc': + return parseFloat(a.precio_ud) - parseFloat(b.precio_ud); + + case 'price-desc': + return parseFloat(b.precio_ud) - parseFloat(a.precio_ud); + + case 'distance': + if (!searchFilters.userLocation) return 0; + + const coordsA = getValidCoordinates(a.coordenates_vendedor); + const coordsB = getValidCoordinates(b.coordenates_vendedor); + + if (!coordsA && !coordsB) return 0; + if (!coordsA) return 1; + if (!coordsB) return -1; + + const distanceA = calculateDistance( + searchFilters.userLocation.lat, searchFilters.userLocation.lng, + coordsA.lat, coordsA.lng + ); + const distanceB = calculateDistance( + searchFilters.userLocation.lat, searchFilters.userLocation.lng, + coordsB.lat, coordsB.lng + ); + + return distanceA - distanceB; + + case 'newest': + return new Date(b.created_at || b.fecha_creacion || 0) - new Date(a.created_at || a.fecha_creacion || 0); + + case 'relevance': + default: + // Relevancia basada en coincidencia de búsqueda + if (!searchFilters.searchText.trim()) return 0; + + const searchTerm = searchFilters.searchText.toLowerCase(); + const titleMatchA = a.titulo?.toLowerCase().includes(searchTerm); + const titleMatchB = b.titulo?.toLowerCase().includes(searchTerm); + + if (titleMatchA && !titleMatchB) return -1; + if (!titleMatchA && titleMatchB) return 1; + + return 0; + } + }); + + return filtered; + }, [offers, searchFilters]); + + // Función para actualizar filtros + const updateFilter = (key, value) => { + setSearchFilters(prev => ({ + ...prev, + [key]: value + })); + }; + + // FUNCIÓN CORREGIDA para actualizar rango de precios + const updatePriceRange = (type, value) => { + setSearchFilters(prev => ({ + ...prev, + priceRange: { + ...prev.priceRange, + [type]: value + } + })); + }; + + + const clearFilters = () => { + setSearchFilters(prev => ({ + ...prev, + searchText: '', + sortBy: 'relevance', + priceRange: { min: '', max: '' }, + productType: 'all', + availableOnly: false + })); + }; + + + const handleClick = (offer) => { + return (e) => { + e.preventDefault(); + + + const existeOferta = store.ofertas && store.ofertas.some(oferta => oferta.id === offer.id); + + if (!existeOferta) { + dispatch({ + type: "add_oferta", + payload: offer + }); + } + + navigate(`/oferta/${offer.id}`); + }; + }; + + return ( + <> + + +
+
+ + {/* BARRA DE BÚSQUEDA AVANZADA */} +
+
+

🔍 Encuentra tu Cosecha Perfecta

+

Busca por producto, ubicación o precio

+
+ + {/* Búsqueda por texto */} +
+
+ updateFilter('searchText', e.target.value)} + className="search-input" + /> + 🌾 +
+
+ + {/* Filtros en una fila */} +
+
+ + {/* Ordenar por */} +
+ + +
+ + {/* Tipo de producto */} +
+ + +
+ + {/* Rango de precios */} +
+ +
+ updatePriceRange('min', e.target.value)} + className="price-input" + min="0" + step="0.01" + /> + - + updatePriceRange('max', e.target.value)} + className="price-input" + min="0" + step="0.01" + /> +
+
+ + {/* Solo disponibles */} +
+ +
+ + {/* Botón limpiar */} +
+ +
+
+
+ + {/* Resumen de resultados */} +
+ + 📊 {filteredAndSortedOffers.length} productos encontrados + {searchFilters.searchText && ` para "${searchFilters.searchText}"`} + + {searchFilters.userLocation && searchFilters.sortBy === 'distance' && ( + + 📍 Ordenados por cercanía a tu ubicación + + )} +
+
+ + {/* SECCIÓN DE OFERTAS */} +
+
+

+ Cosecha Fresca Disponible +

+
+ + 🌱 {filteredAndSortedOffers.length} productos frescos del campo + +
+ + {loading && ( +
+
+
+ Recolectando los productos más frescos del campo español... +
+
+ )} + + {error && ( +
+ 🚨 Error en la cosecha: {error} +
+ )} + + {!loading && !error && ( +
+ {filteredAndSortedOffers.length === 0 ? ( +
+
🔍
+

No se encontraron productos

+

+ Prueba con otros términos de búsqueda o ajusta los filtros +

+ +
+ ) : ( + filteredAndSortedOffers.map((offer) => { + const coordinates = getValidCoordinates(offer.coordenates_vendedor); + let distance = null; + + // Calcular distancia si tenemos ubicación del usuario + if (searchFilters.userLocation && coordinates) { + distance = calculateDistance( + searchFilters.userLocation.lat, + searchFilters.userLocation.lng, + coordinates.lat, + coordinates.lng + ); + } + + return ( +
+ {coordinates ? ( +
+
+ 📍 Finca ubicada en campo español + {distance && ( +
+ 🚗 A {distance.toFixed(1)} km de ti +
+ )} +
+ + + +
+ 🚜 +
+
+
+
+
+ ) : ( +
+ 🗺️ Finca en ubicación privada - Contacta directamente con el agricultor +
+ )} + +
+
+ {getCropIcon(offer.titulo)} + {offer.titulo} +
+

+ 📝 {offer.descripcion} +

+
+ + 💰 €{offer.precio_ud} / {offer.ud} + + + {offer.esta_realizada ? '🔴 Vendido' : '🟢 Disponible ahora'} + +
+
+
+ ); + }) + )} +
+ )} +
+
+
+ + ); +}; \ No newline at end of file diff --git a/src/front/pages/ComprarVender.jsx b/src/front/pages/ComprarVender.jsx new file mode 100644 index 0000000000..6aa1bd4d93 --- /dev/null +++ b/src/front/pages/ComprarVender.jsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; + +export const CompraVenta = () => { + const [filters, setFilters] = useState({ + cereal: "", + precioMin: "", + precioMax: "", + ciudad: "" + }); + + const handleChange = (e) => { + setFilters({ ...filters, [e.target.name]: e.target.value }); + }; + + const handleSearch = () => { + console.log("Filtros aplicados:", filters); + }; + + return ( +
+

Búsqueda de cereales para comprar o vender

+ +
+ + + +
+ + +
+ + + + +
+
+ ); +}; diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index 341ed21768..287efdf0a4 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,52 +1,561 @@ -import React, { useEffect } from "react" -import rigoImageUrl from "../assets/img/rigo-baby.jpg"; -import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import useGlobalReducer from '../hooks/useGlobalReducer'; +import { APIProvider, useMap, Map } from '@vis.gl/react-google-maps'; +import { AdvancedMarker } from '@vis.gl/react-google-maps'; +import { beautifulStyles } from "../styles/beautifulStyles"; + + export const Home = () => { + const { store, dispatch } = useGlobalReducer(); + const [offers, setOffers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Form state + const [form, setForm] = useState({ + titulo: "", + descripcion: "", + precio_ud: "", + ud: "", + img_cosecha: "", + esta_realizada: false + }); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + useEffect(() => { + const fetchOffers = async () => { + try { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + if (!backendUrl) { + throw new Error("VITE_BACKEND_URL is not defined"); + } + + // Use the environment variable instead of hardcoded URL + const res = await fetch(`${backendUrl}api/user/ofertas`); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const data = await res.json(); + setOffers(data.ofertas || []); + } catch (err) { + console.error('Error fetching offers:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchOffers(); + }, []); + + const handleChange = (e) => { + const { name, value, type, checked } = e.target; + setForm(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value + })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setSubmitting(true); + setSubmitError(null); + + try { + const token = localStorage.getItem("jwt_token"); + if (!token) { + throw new Error("No authentication token found"); + } + + const backendUrl = import.meta.env.VITE_BACKEND_URL; + const response = await fetch(`${backendUrl}api/user/ofertas`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify(form) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || `HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('Offer created successfully:', data); + + // Reset form on success + setForm({ + titulo: "", + descripcion: "", + precio_ud: "", + ud: "", + img_cosecha: "", + esta_realizada: false + }); + + // Refresh offers list + const updatedOffers = await fetch(`${backendUrl}api/user/ofertas`); + const updatedData = await updatedOffers.json(); + setOffers(updatedData.ofertas || []); + + } catch (err) { + console.error('Error submitting offer:', err); + setSubmitError(err.message); + } finally { + setSubmitting(false); + } + }; + + const isValidCoordinate = (num) => { + return !isNaN(num) && isFinite(num); + }; + + const getValidCoordinates = (coordenates_vendedor) => { + if (!coordenates_vendedor) return null; + + let lat, lng; + + if (typeof coordenates_vendedor === 'string') { + const cleaned = coordenates_vendedor.replace(/[{}]/g, ''); + const coords = cleaned.split(','); + + if (coords.length >= 2) { + lat = parseFloat(coords[0].trim()); + lng = parseFloat(coords[1].trim()); + } else { + return null; + } + } else if (typeof coordenates_vendedor === 'object' && !Array.isArray(coordenates_vendedor)) { + lat = parseFloat(coordenates_vendedor.lat || coordenates_vendedor.latitude); + lng = parseFloat(coordenates_vendedor.lng || coordenates_vendedor.longitude || coordenates_vendedor.lon); + } else if (Array.isArray(coordenates_vendedor) && coordenates_vendedor.length >= 2) { + lat = parseFloat(coordenates_vendedor[0]); + lng = parseFloat(coordenates_vendedor[1]); + } else { + return null; + } + + if (isValidCoordinate(lat) && isValidCoordinate(lng)) { + if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + return { lat, lng }; + } + } + + return null; + }; + + const getCropIcon = (title) => { + if (!title) return 🌱; + + const titleLower = title.toLowerCase(); + + const iconMap = { + 'trigo|cereal|avena': '🌾', + 'tomate|verdura|hortaliza': '🍅', + 'fruta|manzana|pera': '🍎', + 'uva|vino|viña': '🍇', + 'oliva|aceite|aceituna': '🫒', + 'naranja|limón|cítrico': '🍊', + 'girasol|flor': '🌻', + 'maíz|grano': '🌽', + 'lechuga|ensalada': '🥬', + 'zanahoria|tubérculo': '🥕' + }; + + for (const [keywords, icon] of Object.entries(iconMap)) { + if (keywords.split('|').some(keyword => titleLower.includes(keyword))) { + return {icon}; + } + } + + return 🌱; + }; + + return ( +
+ +
+
+ + {/* HEADER */} +
+

+ Mercado del Campo Español +

+

+ Donde la tradición agrícola se encuentra con la innovación digital +

+
+ +
+
+ + {/* FORM SECTION */} +
+ {store.user ? ( + <> +
+

+ Comparte los Frutos de tu Tierra +

+

+ Conecta tu cosecha directamente con quienes la valoran +

+
+ + {submitError && ( +
+ ⚠️ {submitError} +
+ )} + +
+
+ + +
+ +
+ +