From a4a53bcd04dd9ac0dd74667bb9f42c291fb1b4eb Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Thu, 27 Feb 2025 23:48:32 +0000 Subject: [PATCH 01/45] base de datos creada --- Pipfile | 3 +- Pipfile.lock | 911 +++++++++++------- migrations/README | 1 + migrations/alembic.ini | 50 + migrations/env.py | 113 +++ migrations/script.py.mako | 24 + ...cializaci\303\263n_de_la_base_de_datos.py" | 74 ++ src/api/admin.py | 4 +- src/api/models.py | 68 +- src/instance/test.db | Bin 0 -> 77824 bytes 10 files changed, 896 insertions(+), 352 deletions(-) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 "migrations/versions/f1cc659f7467_inicializaci\303\263n_de_la_base_de_datos.py" create mode 100644 src/instance/test.db diff --git a/Pipfile b/Pipfile index b461e2e4ee..1804939d28 100644 --- a/Pipfile +++ b/Pipfile @@ -4,10 +4,11 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] +sqlalchemy = "*" [packages] flask = "*" -sqlalchemy = "==1.4.46" +sqlalchemy = "*" flask-sqlalchemy = "*" flask-migrate = "*" flask-swagger = "*" diff --git a/Pipfile.lock b/Pipfile.lock index a391864e9d..3ab6ce8fdd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "74f92d76f687bb774828613a3a513123fe2ffdb429b95b351d29721dddfd3fb8" + "sha256": "9887cedd6a60234a4486277c24baf287bcb7795f76b567fcd84b6175baba6748" }, "pipfile-spec": 6, "requires": { @@ -18,73 +18,91 @@ "default": { "alembic": { "hashes": [ - "sha256:6880dec4f28dd7bd999d2ed13fbe7c9d4337700a44d11a524c0ce0c59aaf0dbd", - "sha256:e8a6ff9f3b1887e1fed68bfb8fb9a000d8f61c21bdcc85b67bb9f87fcbc4fce3" + "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", + "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" ], - "markers": "python_version >= '3.7'", - "version": "==1.9.2" + "markers": "python_version >= '3.8'", + "version": "==1.14.1" + }, + "blinker": { + "hashes": [ + "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", + "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" + ], + "markers": "python_version >= '3.9'", + "version": "==1.9.0" }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], "markers": "python_version >= '3.6'", - "version": "==2022.12.7" + "version": "==2025.1.31" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.3" + "version": "==8.1.8" }, "cloudinary": { "hashes": [ - "sha256:f52a1f5eb2c6820f13aa01c109caa5937ad3fd6caf5967817d0ef6c113403afc" + "sha256:ba223705409b2aaddd5196c2184d65f50a83dffcba3b94f3727658ff6a0172a3", + "sha256:e4191b470c5bae55542b64e0a78659af42971880294456dca480bc974fa9280a" ], "index": "pypi", - "version": "==1.31.0" + "version": "==1.42.2" }, "flask": { "hashes": [ - "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b", - "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526" + "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", + "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" ], "index": "pypi", - "version": "==2.2.2" + "version": "==3.1.0" }, "flask-admin": { "hashes": [ - "sha256:424ffc79b7b0dfff051555686ea12e86e48dffacac14beaa319fb4502ac40988" + "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369", + "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406" ], "index": "pypi", - "version": "==1.6.0" + "version": "==1.6.1" }, "flask-cors": { "hashes": [ - "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438", - "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de" + "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", + "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c" ], "index": "pypi", - "version": "==3.0.10" + "version": "==5.0.1" + }, + "flask-jwt-extended": { + "hashes": [ + "sha256:63a28fc9731bcc6c4b8815b6f954b5904caa534fc2ae9b93b1d3ef12930dca95", + "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2" + ], + "index": "pypi", + "version": "==4.6.0" }, "flask-migrate": { "hashes": [ - "sha256:8662a9dd391ce36deeaf3265987319c20fdb4c8a45306a32ba4f8224459abed4", - "sha256:a0062c8d3f32de02847086b46cfc389412f78c71c89a619ebd7097e89d72ea4b" + "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", + "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d" ], "index": "pypi", - "version": "==4.0.3" + "version": "==4.1.0" }, "flask-sqlalchemy": { "hashes": [ - "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec", - "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a" + "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", + "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312" ], "index": "pypi", - "version": "==3.0.3" + "version": "==3.1.1" }, "flask-swagger": { "hashes": [ @@ -96,384 +114,593 @@ }, "greenlet": { "hashes": [ - "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", - "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", - "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", - "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", - "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", - "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", - "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", - "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", - "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", - "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", - "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", - "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", - "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", - "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", - "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", - "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", - "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", - "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", - "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", - "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", - "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", - "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", - "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", - "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", - "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", - "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", - "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", - "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", - "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", - "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", - "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", - "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", - "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", - "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", - "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", - "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", - "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", - "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", - "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", - "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", - "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", - "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", - "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", - "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", - "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", - "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", - "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", - "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", - "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", - "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", - "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", - "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", - "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", - "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", - "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", - "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", - "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", - "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", - "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", - "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" - ], - "markers": "python_version >= '3' 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": "==2.0.2" + "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" }, "gunicorn": { "hashes": [ - "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", - "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" ], "index": "pypi", - "version": "==20.1.0" + "version": "==23.0.0" }, "itsdangerous": { "hashes": [ - "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", - "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "markers": "python_version >= '3.8'", + "version": "==2.2.0" }, "jinja2": { "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", + "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" ], "markers": "python_version >= '3.7'", - "version": "==3.1.2" + "version": "==3.1.5" }, "mako": { "hashes": [ - "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", - "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" + "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", + "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.4" + "markers": "python_version >= '3.8'", + "version": "==1.3.9" }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" }, "psycopg2-binary": { "hashes": [ - "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50", - "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425", - "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f", - "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0", - "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460", - "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41", - "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85", - "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd", - "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0", - "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd", - "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147", - "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c", - "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903", - "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba", - "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632", - "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577", - "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c", - "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7", - "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867", - "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2", - "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9", - "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff", - "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a", - "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302", - "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1", - "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79", - "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835", - "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42", - "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e", - "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61", - "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32", - "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68", - "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1", - "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60", - "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8", - "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b", - "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a", - "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec", - "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5", - "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2", - "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16", - "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5", - "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6", - "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1", - "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503", - "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b", - "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d", - "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28", - "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4", - "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5", - "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5", - "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e", - "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f", - "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636", - "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d", - "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64", - "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb", - "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882", - "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720", - "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896", - "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267", - "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7", - "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f", - "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91", - "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c", - "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24", - "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee", - "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d", - "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b", - "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935", - "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69" + "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", + "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", + "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", + "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", + "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", + "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", + "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", + "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", + "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", + "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", + "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", + "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", + "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", + "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", + "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", + "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", + "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", + "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", + "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", + "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", + "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", + "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", + "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", + "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", + "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", + "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", + "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", + "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", + "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", + "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", + "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", + "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", + "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", + "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", + "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", + "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", + "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", + "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", + "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", + "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", + "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", + "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", + "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", + "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", + "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", + "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", + "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", + "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", + "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", + "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", + "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", + "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", + "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", + "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", + "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", + "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", + "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", + "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", + "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", + "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", + "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", + "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", + "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", + "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", + "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", + "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", + "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", + "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" ], "index": "pypi", - "version": "==2.9.5" + "version": "==2.9.10" }, - "python-dotenv": { + "pyjwt": { "hashes": [ - "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49", - "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a" + "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" ], - "index": "pypi", - "version": "==0.21.1" + "markers": "python_version >= '3.9'", + "version": "==2.10.1" }, - "pyyaml": { + "python-dotenv": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" ], - "markers": "python_version >= '3.6'", - "version": "==6.0" + "index": "pypi", + "version": "==1.0.1" }, - "setuptools": { + "pyyaml": { "hashes": [ - "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378", - "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], - "markers": "python_version >= '3.7'", - "version": "==67.1.0" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "version": "==1.17.0" }, "sqlalchemy": { "hashes": [ - "sha256:07e48cbcdda6b8bc7a59d6728bd3f5f574ffe03f2c9fb384239f3789c2d95c2e", - "sha256:18cafdb27834fa03569d29f571df7115812a0e59fd6a3a03ccb0d33678ec8420", - "sha256:1b1e5e96e2789d89f023d080bee432e2fef64d95857969e70d3cadec80bd26f0", - "sha256:315676344e3558f1f80d02535f410e80ea4e8fddba31ec78fe390eff5fb8f466", - "sha256:31de1e2c45e67a5ec1ecca6ec26aefc299dd5151e355eb5199cd9516b57340be", - "sha256:3d94682732d1a0def5672471ba42a29ff5e21bb0aae0afa00bb10796fc1e28dd", - "sha256:3ec187acf85984263299a3f15c34a6c0671f83565d86d10f43ace49881a82718", - "sha256:4847f4b1d822754e35707db913396a29d874ee77b9c3c3ef3f04d5a9a6209618", - "sha256:4d112b0f3c1bc5ff70554a97344625ef621c1bfe02a73c5d97cac91f8cd7a41e", - "sha256:51e1ba2884c6a2b8e19109dc08c71c49530006c1084156ecadfaadf5f9b8b053", - "sha256:535377e9b10aff5a045e3d9ada8a62d02058b422c0504ebdcf07930599890eb0", - "sha256:5dbf17ac9a61e7a3f1c7ca47237aac93cabd7f08ad92ac5b96d6f8dea4287fc1", - "sha256:5f752676fc126edc1c4af0ec2e4d2adca48ddfae5de46bb40adbd3f903eb2120", - "sha256:64cb0ad8a190bc22d2112001cfecdec45baffdf41871de777239da6a28ed74b6", - "sha256:6913b8247d8a292ef8315162a51931e2b40ce91681f1b6f18f697045200c4a30", - "sha256:69fac0a7054d86b997af12dc23f581cf0b25fb1c7d1fed43257dee3af32d3d6d", - "sha256:7001f16a9a8e06488c3c7154827c48455d1c1507d7228d43e781afbc8ceccf6d", - "sha256:7b81b1030c42b003fc10ddd17825571603117f848814a344d305262d370e7c34", - "sha256:7f8267682eb41a0584cf66d8a697fef64b53281d01c93a503e1344197f2e01fe", - "sha256:887865924c3d6e9a473dc82b70977395301533b3030d0f020c38fd9eba5419f2", - "sha256:9167d4227b56591a4cc5524f1b79ccd7ea994f36e4c648ab42ca995d28ebbb96", - "sha256:939f9a018d2ad04036746e15d119c0428b1e557470361aa798e6e7d7f5875be0", - "sha256:955162ad1a931fe416eded6bb144ba891ccbf9b2e49dc7ded39274dd9c5affc5", - "sha256:984ee13543a346324319a1fb72b698e521506f6f22dc37d7752a329e9cd00a32", - "sha256:9883f5fae4fd8e3f875adc2add69f8b945625811689a6c65866a35ee9c0aea23", - "sha256:a1ad90c97029cc3ab4ffd57443a20fac21d2ec3c89532b084b073b3feb5abff3", - "sha256:a3714e5b33226131ac0da60d18995a102a17dddd42368b7bdd206737297823ad", - "sha256:ae067ab639fa499f67ded52f5bc8e084f045d10b5ac7bb928ae4ca2b6c0429a5", - "sha256:b33ffbdbbf5446cf36cd4cc530c9d9905d3c2fe56ed09e25c22c850cdb9fac92", - "sha256:b6e4cb5c63f705c9d546a054c60d326cbde7421421e2d2565ce3e2eee4e1a01f", - "sha256:b7f4b6aa6e87991ec7ce0e769689a977776db6704947e562102431474799a857", - "sha256:c04144a24103135ea0315d459431ac196fe96f55d3213bfd6d39d0247775c854", - "sha256:c522e496f9b9b70296a7675272ec21937ccfc15da664b74b9f58d98a641ce1b6", - "sha256:c5a99282848b6cae0056b85da17392a26b2d39178394fc25700bcf967e06e97a", - "sha256:c7a46639ba058d320c9f53a81db38119a74b8a7a1884df44d09fbe807d028aaf", - "sha256:d4b1cc7835b39835c75cf7c20c926b42e97d074147c902a9ebb7cf2c840dc4e2", - "sha256:d4d164df3d83d204c69f840da30b292ac7dc54285096c6171245b8d7807185aa", - "sha256:d61e9ecc849d8d44d7f80894ecff4abe347136e9d926560b818f6243409f3c86", - "sha256:d68e1762997bfebf9e5cf2a9fd0bcf9ca2fdd8136ce7b24bbd3bbfa4328f3e4a", - "sha256:e3c1808008124850115a3f7e793a975cfa5c8a26ceeeb9ff9cbb4485cac556df", - "sha256:f8cb80fe8d14307e4124f6fad64dfd87ab749c9d275f82b8b4ec84c84ecebdbe" + "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" ], "index": "pypi", - "version": "==1.4.46" + "version": "==2.0.38" }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", - "version": "==4.4.0" + "version": "==4.12.2" }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", + "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.14" + "markers": "python_version >= '3.9'", + "version": "==2.3.0" }, "werkzeug": { "hashes": [ - "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", - "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" ], - "markers": "python_version >= '3.7'", - "version": "==2.2.2" + "markers": "python_version >= '3.9'", + "version": "==3.1.3" }, "wtforms": { "hashes": [ - "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc", - "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b" + "sha256:bf831c042829c8cdbad74c27575098d541d039b1faa74c771545ecac916f2c07", + "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9" ], - "markers": "python_version >= '3.7'", - "version": "==3.0.1" + "index": "pypi", + "version": "==3.1.2" } }, - "develop": {} + "develop": { + "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" + }, + "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" + ], + "index": "pypi", + "version": "==2.0.38" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "index": "pypi", + "version": "==4.12.2" + } + } } diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000000..0e04844159 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000000..ec9d45c26a --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000000..4c9709271b --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000000..2c0156303a --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git "a/migrations/versions/f1cc659f7467_inicializaci\303\263n_de_la_base_de_datos.py" "b/migrations/versions/f1cc659f7467_inicializaci\303\263n_de_la_base_de_datos.py" new file mode 100644 index 0000000000..eb1132455a --- /dev/null +++ "b/migrations/versions/f1cc659f7467_inicializaci\303\263n_de_la_base_de_datos.py" @@ -0,0 +1,74 @@ +"""Inicialización de la base de datos + +Revision ID: f1cc659f7467 +Revises: +Create Date: 2025-02-27 23:42:10.245977 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f1cc659f7467' +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(length=80), nullable=False), + sa.Column('first_name', sa.String(length=120), nullable=False), + sa.Column('last_name', sa.String(length=120), nullable=False), + sa.Column('birthdate', sa.String(length=80), nullable=False), + sa.Column('country', sa.String(length=120), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('birthdate'), + sa.UniqueConstraint('country'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('first_name'), + sa.UniqueConstraint('last_name') + ) + op.create_table('accounts', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('balance', sa.Integer(), nullable=False), + sa.Column('coin', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=80), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('balance'), + sa.UniqueConstraint('coin'), + sa.UniqueConstraint('type') + ) + op.create_table('account_details', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('accounts_id', sa.Integer(), nullable=False), + sa.Column('detail', sa.String(length=80), nullable=False), + sa.Column('amount', sa.Integer(), nullable=False), + sa.Column('coin', sa.String(length=120), nullable=False), + sa.Column('type', sa.String(length=80), nullable=False), + sa.Column('date', sa.String(length=80), nullable=False), + sa.Column('time', sa.String(length=80), nullable=False), + sa.ForeignKeyConstraint(['accounts_id'], ['accounts.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('amount'), + sa.UniqueConstraint('coin'), + sa.UniqueConstraint('date'), + sa.UniqueConstraint('time'), + sa.UniqueConstraint('type') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('account_details') + op.drop_table('accounts') + op.drop_table('user') + # ### end Alembic commands ### diff --git a/src/api/admin.py b/src/api/admin.py index 3eecb64140..1056fdfa49 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,7 +1,7 @@ import os from flask_admin import Admin -from .models import db, User +from .models import db, User, Accounts, Account_details from flask_admin.contrib.sqla import ModelView def setup_admin(app): @@ -12,6 +12,8 @@ def setup_admin(app): # Add your models here, for example this is how we add a the User model to the admin admin.add_view(ModelView(User, db.session)) + admin.add_view(ModelView(Accounts, db.session)) + admin.add_view(ModelView(Account_details, db.session)) # You can duplicate that line to add mew models # admin.add_view(ModelView(YourModelName, db.session)) \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index dccd8421ee..a8bf01a802 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,71 @@ +from typing import List +from sqlalchemy.orm import declarative_base, Mapped, mapped_column, relationship from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import String, create_engine, ForeignKey db = SQLAlchemy() class User(db.Model): - id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), unique=True, nullable=False) - password = db.Column(db.String(80), unique=False, nullable=False) - is_active = db.Column(db.Boolean(), unique=False, nullable=False) - - def __repr__(self): - return f'' + __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(String(80),nullable=False) + first_name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) + last_name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) + birthdate: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) + country: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) + accounts: Mapped[List["Accounts"]] = relationship() def serialize(self): return { "id": self.id, "email": self.email, + "first_name": self.first_name, + "last_name": self.last_name, + "birthdate": self.birthdate, + "country": self.country, # do not serialize the password, its a security breach - } \ No newline at end of file + } + +class Accounts(db.Model): + __tablename__ = 'accounts' + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) + name: Mapped[str] = mapped_column(String(80),nullable=False) + balance: Mapped[int] = mapped_column(unique=True, nullable=False) + coin: Mapped[int] = mapped_column(unique=True, nullable=False) + type: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) + accounts: Mapped[List["Account_details"]] = relationship() + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "name": self.name, + "balance": self.balance, + "coin": self.coin, + "type": self.type + } + +class Account_details(db.Model): + __tablename__ = 'account_details' + id: Mapped[int] = mapped_column(primary_key=True) + accounts_id: Mapped[int] = mapped_column(ForeignKey("accounts.id")) + detail: Mapped[str] = mapped_column(String(80),nullable=False) + amount: Mapped[int] = mapped_column(unique=True, nullable=False) + coin: Mapped[int] = mapped_column(String(120), unique=True, nullable=False) + type: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) + date: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) + time: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) + + def serialize(self): + return { + "id": self.id, + "accounts_id": self.accounts_id, + "detail": self.detail, + "amount": self.amount, + "coin": self.coin, + "type": self.type, + "date": self.date, + "time": self.time, + } \ No newline at end of file diff --git a/src/instance/test.db b/src/instance/test.db new file mode 100644 index 0000000000000000000000000000000000000000..d42befdc75fc03cd1e0fc1c9a464cced7122f194 GIT binary patch literal 77824 zcmeI)%WfK17{KuvU%*VbIF2k8R{7f#M{vyc@lHxoWp#-@8iG`wvUevT;I`8 z`-86S>pRM-qN>UxT~`!kSsZu75!{xYVggB|Cg0}&wk3}{#DkJAC+7t_j~%c z?BCfxvOj0t>?7^Z^oMja{aNy(_KWsH%cuTI{W@bVP9cB*0tg_0Kv>}2VJ4a1+*Cg_ zefy;2*sWIoyyshO$G6>%7rC3QnMSo?=#A>*14ECT)QeimZR`8>hVj%e_4;8$uQv}4 zKG(I>sHbO5Ie+z-vZt9@S0jxIG+ud;2TR!5f> zs=SF}m6sw)aVwq3*EZ&qqGttF;AOUMebRs+?_PRK`=kLuroCJ55{t?F`nvkQKTNR< zQ65X7)LsTYHLXrptc~NPV`9}gvpw(o{-8bX5nXvs-GS#@p#qqgq+`$Ra^epBZ)dJz z!?k>HF}{$AN4c&lf)!{yeI(UlDIQ#e+J+vXmj5FRHIIW5wNY3}QLcVx$8VR>%HZP_tVSeo0hEITg$P*R7lqHj}|3rEN4p13XRJ7?gz z{hl4ZyAZ0i$Vs`HjD}gg^X@oxJKN#c#M)u~xM5bsI-o~pu+Cn!^iX22%qL$T7G|!I z;cKP*x{6GFVCPD-dKmY** z5I_I{1Q0*~0R-6pQv(PffB*srAb Date: Fri, 28 Feb 2025 11:28:19 +0000 Subject: [PATCH 02/45] container del login creado --- .vscode/settings.json | 3 +++ src/front/js/component/login-form.js | 8 ++++++++ src/front/js/layout.js | 9 ++------- src/front/js/pages/login.js | 20 ++++++++++++++++++++ src/front/styles/login.css | 17 +++++++++++++++++ 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 src/front/js/component/login-form.js create mode 100644 src/front/js/pages/login.js create mode 100644 src/front/styles/login.css diff --git a/.vscode/settings.json b/.vscode/settings.json index 24da33c3e2..7bd07e2e82 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,8 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "workbench.editorAssociations": { "*.md": "vscode.markdown.preview.editor" + }, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" } } diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js new file mode 100644 index 0000000000..9e82834197 --- /dev/null +++ b/src/front/js/component/login-form.js @@ -0,0 +1,8 @@ +import React from "react"; + +export const LoginForm = () => { + + return ( +

LOGIN AQUÍ

+ ); +}; diff --git a/src/front/js/layout.js b/src/front/js/layout.js index d42289f0ee..de9fc439c3 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -10,6 +10,7 @@ import injectContext from "./store/appContext"; import { Navbar } from "./component/navbar"; import { Footer } from "./component/footer"; +import { Login } from "./pages/login"; //create your first component const Layout = () => { @@ -20,20 +21,14 @@ const Layout = () => { if(!process.env.BACKEND_URL || process.env.BACKEND_URL == "") return ; return ( -
- - - } path="/" /> + } path="/" /> } path="/demo" /> } path="/single/:theid" /> Not found!} /> -
- -
); }; diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js new file mode 100644 index 0000000000..e03a231627 --- /dev/null +++ b/src/front/js/pages/login.js @@ -0,0 +1,20 @@ +import React from "react"; +import { LoginForm } from "../component/login-form"; +import "../../styles/login.css"; + +export const Login = () => { + + return ( +
+
+
+

OPTIMA

+

"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

+
+
+ +
+
+
+ ); +}; diff --git a/src/front/styles/login.css b/src/front/styles/login.css new file mode 100644 index 0000000000..9b87e8396e --- /dev/null +++ b/src/front/styles/login.css @@ -0,0 +1,17 @@ +.container-login{ + height: 100vh; + width: 100vw; + display: flex; + justify-content: center; +} +.login{ + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} +.login-left{ + display: flex; + flex-direction: column; + text-align: center; +} \ No newline at end of file From 0eb772c22540e2cb66293641effb1e72aee42e57 Mon Sep 17 00:00:00 2001 From: Luis Malizia Date: Fri, 28 Feb 2025 12:21:04 +0000 Subject: [PATCH 03/45] se crearon los endpoinst de uno y todos los usuarios y el endpoint de login --- src/api/routes.py | 149 +++++++++++++++++++++++++++++++++++++++++-- src/instance/test.db | Bin 77824 -> 77824 bytes 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..fa718da291 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -5,18 +5,159 @@ from api.models import db, User from api.utils import generate_sitemap, APIException from flask_cors import CORS +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required,verify_jwt_in_request api = Blueprint('api', __name__) - # Allow CORS requests to this API CORS(api) -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): +# @api.route('/hello', methods=['POST', 'GET']) +# def handle_hello(): + +# response_body = { +# "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" +# } + +# return jsonify(response_body), 200 + + +@api.route('/users', methods=['GET']) +def get_users(): + + data = db.session.scalars(db.select(User)).all() + result = list(map(lambda item: item.serialize(),data)) + + if result == []: + return jsonify({"msg":"Usuario no encontrado"}), 404 response_body = { - "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" + "results": result } return jsonify(response_body), 200 + + +@api.route('/user/', methods=['GET']) +def get_one_user(user_id): + try: + user = db.session.execute(db.select(User).filter_by(id=user_id)).scalar_one() + return jsonify({"result":user.serialize()}), 200 + except: + return jsonify({"msg":"Usuario o contraseña incorrecta"}), 404 + + + +@api.route("/login", methods=["POST"]) +def login(): + + email = request.json.get("email", None) + password = request.json.get("password", None) + try: + user = db.session.execute(db.select(User).filter_by(email=email)).scalar_one() + if password != user.password: + return jsonify({"msg": "Bad email or password"}), 401 + # access_token = create_access_token(identity=email) + return jsonify({"msg": "Succesfull access"}) + except: + return jsonify({"msg": "this user does not exist"}), 404 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +#espacio de laura -> \ No newline at end of file diff --git a/src/instance/test.db b/src/instance/test.db index d42befdc75fc03cd1e0fc1c9a464cced7122f194..9568ea8e9d70672f29fe9ddf91913b19c945292f 100644 GIT binary patch delta 383 zcmZp8z|!!5Wr8##`$QRMM)r*fOY}LI_`4YRkMMVG78I!CueV_0V9=Hnmz9+)NKDGf zcSz4o%*@eC&d)V8G6o{BU}0%$QDRPNv5|qkfw{h=xo1gYPG+JNBL{=7q^Pv4d`@X* zac(ZURv)0KFHm7sW+GIhMObQHYE@}!P9hfr0|OKPa|V9q=loxQ4!O;*%EK(n2(y5R zn^~3^#%j~8~z(Wm1p=(gqgJ&(fq|E z%&g0a?qY}??-=-h@W0zEXz++%j+0p&qLYc8S(Fpx7q~mV^1lGPLjvSvSlBReF-vno Py}QW3VbKJKMGgu88&YbT delta 33 rcmV++0N($A-~@o+1dtm61(6&>0R^#Oq%X4sko-TQpdg~qK%&rK)BFv* From 900a2573a25ab52e28cb5f625a5136c5cd3dbae7 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:23:41 +0000 Subject: [PATCH 04/45] =?UTF-8?q?se=20cre=C3=B3=20el=20login,=20falta=20co?= =?UTF-8?q?nectarlo=20con=20el=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/component/login-form.js | 30 +++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js index 9e82834197..a50c819d58 100644 --- a/src/front/js/component/login-form.js +++ b/src/front/js/component/login-form.js @@ -1,8 +1,32 @@ -import React from "react"; +import React, { useState, useContext, useEffect } from "react"; +import { Context } from "../store/appContext"; +import { Link, useNavigate } from "react-router-dom"; export const LoginForm = () => { + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const { store, actions } = useContext(Context) + let navigate = useNavigate(); + + async function handleSubmit(e) { + e.preventDefault() + // actions.login(email, password); + console.log(email); + console.log(password); + } return ( -

LOGIN AQUÍ

+
+
+ + setEmail(e.target.value)} value={email} /> +
We'll never share your email with anyone else.
+
+
+ + setPassword(e.target.value)} value={password} /> +
+ +
); -}; +}; \ No newline at end of file From b8885e8b9a7d266df2ebc77af08428bf5fd27a84 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Fri, 28 Feb 2025 14:58:46 +0000 Subject: [PATCH 05/45] sidebar inicial --- src/front/js/component/sidebar.js | 56 +++++++++++++++++++++++++++++ src/front/js/layout.js | 20 ++++++----- src/front/js/pages/principalPage.js | 8 +++++ src/front/styles/sidebar.css | 49 +++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 src/front/js/component/sidebar.js create mode 100644 src/front/js/pages/principalPage.js create mode 100644 src/front/styles/sidebar.css diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js new file mode 100644 index 0000000000..0033d341a2 --- /dev/null +++ b/src/front/js/component/sidebar.js @@ -0,0 +1,56 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import "/src/front/styles/sidebar.css"; + + +export const Sidebar = () => { + return ( +
+ + Sidebar + +
+
    +
  • + + 🏠 Home + +
  • +
  • + + 📊 Dashboard + +
  • +
  • + + 📦 Orders + +
  • +
  • + + 🛒 Products + +
  • +
  • + + 👥 Customers + +
  • +
+
+
+ + User + mdo + +
    +
  • New project...
  • +
  • Settings
  • +
  • Profile
  • +

  • +
  • Sign out
  • +
+
+
+ ); +}; diff --git a/src/front/js/layout.js b/src/front/js/layout.js index de9fc439c3..10ae67f8c1 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -11,6 +11,7 @@ import injectContext from "./store/appContext"; import { Navbar } from "./component/navbar"; import { Footer } from "./component/footer"; import { Login } from "./pages/login"; +import { PrincipalPage } from "./pages/principalPage" //create your first component const Layout = () => { @@ -18,17 +19,18 @@ const Layout = () => { // you can set the basename on the .env file located at the root of this project, E.g: BASENAME=/react-hello-webapp/ const basename = process.env.BASENAME || ""; - if(!process.env.BACKEND_URL || process.env.BACKEND_URL == "") return ; + if (!process.env.BACKEND_URL || process.env.BACKEND_URL == "") return ; return ( - - - } path="/" /> - } path="/demo" /> - } path="/single/:theid" /> - Not found!} /> - - + + + } path="/" /> + } path="/demo" /> + } path="/principal-page" /> + } path="/single/:theid" /> + Not found!} /> + + ); }; diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js new file mode 100644 index 0000000000..0565e462f2 --- /dev/null +++ b/src/front/js/pages/principalPage.js @@ -0,0 +1,8 @@ +import React from "react"; +import { Sidebar } from "../component/sidebar"; + +export const PrincipalPage = () => { + return ( + + ) +} \ No newline at end of file diff --git a/src/front/styles/sidebar.css b/src/front/styles/sidebar.css new file mode 100644 index 0000000000..4b0bf77822 --- /dev/null +++ b/src/front/styles/sidebar.css @@ -0,0 +1,49 @@ + +.sidebar { + width: 280px; + height: 100vh; + background-color: #343a40; + color: white; + padding: 20px; +} + +/* Links del sidebar */ +.sidebar .nav-link { + color: rgba(255, 255, 255, 0.75); + transition: color 0.3s ease; +} + +.sidebar .nav-link:hover, +.sidebar .nav-link.active { + color: white; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 5px; +} + + +.sidebar .nav-link svg, +.sidebar .nav-link span { + margin-right: 8px; +} + + +.sidebar .dropdown-toggle { + display: flex; + align-items: center; +} + +.sidebar .dropdown-toggle img { + border-radius: 50%; +} + +.sidebar .dropdown-menu { + background-color: #212529; +} + +.sidebar .dropdown-menu .dropdown-item { + color: white; +} + +.sidebar .dropdown-menu .dropdown-item:hover { + background-color: rgba(255, 255, 255, 0.1); +} From 2943da58ba9312b93b5e7d3a21228debad6e5be8 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:35:18 +0000 Subject: [PATCH 06/45] se acomodo la info y se crearon los iconos falta acomodar espacios, rutas y api de imagenes --- src/front/js/component/sidebar.js | 58 +++++++++++++++---------------- template.html | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js index 0033d341a2..cf2377ef88 100644 --- a/src/front/js/component/sidebar.js +++ b/src/front/js/component/sidebar.js @@ -6,51 +6,51 @@ import "/src/front/styles/sidebar.css"; export const Sidebar = () => { return (
- - Sidebar - -
+
+ + Optima + logo + +
+
+

Welcome +

+ slogan +

+
+ User +

@usuario-db

+
+
+
  • - 🏠 Home + Cuentas
  • - 📊 Dashboard - -
  • -
  • - - 📦 Orders + Movimientos
  • + + +
    + +
  • - - 🛒 Products + + FAQs
  • - - 👥 Customers + + Sign Out
-
-
- - User - mdo - -
    -
  • New project...
  • -
  • Settings
  • -
  • Profile
  • -

  • -
  • Sign out
  • -
-
+ ); }; diff --git a/template.html b/template.html index 9f8b46a227..6aa917da2c 100755 --- a/template.html +++ b/template.html @@ -7,7 +7,7 @@ - +
From 2d6517fb2cf5509a43700f59b5f014004d22b0a6 Mon Sep 17 00:00:00 2001 From: Luis Malizia Date: Fri, 28 Feb 2025 17:02:34 +0000 Subject: [PATCH 07/45] creado el endpoint de post account pero falta un detalle --- migrations/versions/3c8bf7fb6862_.py | 38 +++++++++++ migrations/versions/d2e65d632be5_.py | 50 +++++++++++++++ src/api/models.py | 24 +++---- src/api/routes.py | 90 ++++++++++++--------------- src/instance/test.db | Bin 77824 -> 102400 bytes 5 files changed, 139 insertions(+), 63 deletions(-) create mode 100644 migrations/versions/3c8bf7fb6862_.py create mode 100644 migrations/versions/d2e65d632be5_.py diff --git a/migrations/versions/3c8bf7fb6862_.py b/migrations/versions/3c8bf7fb6862_.py new file mode 100644 index 0000000000..ac5f574834 --- /dev/null +++ b/migrations/versions/3c8bf7fb6862_.py @@ -0,0 +1,38 @@ +"""empty message + +Revision ID: 3c8bf7fb6862 +Revises: f1cc659f7467 +Create Date: 2025-02-28 14:38:06.815551 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3c8bf7fb6862' +down_revision = 'f1cc659f7467' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('accounts', schema=None) as batch_op: + batch_op.alter_column('coin', + existing_type=sa.INTEGER(), + type_=sa.String(), + existing_nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('accounts', schema=None) as batch_op: + batch_op.alter_column('coin', + existing_type=sa.String(), + type_=sa.INTEGER(), + existing_nullable=False) + + # ### end Alembic commands ### diff --git a/migrations/versions/d2e65d632be5_.py b/migrations/versions/d2e65d632be5_.py new file mode 100644 index 0000000000..8c8ea90c95 --- /dev/null +++ b/migrations/versions/d2e65d632be5_.py @@ -0,0 +1,50 @@ +"""empty message + +Revision ID: d2e65d632be5 +Revises: 3c8bf7fb6862 +Create Date: 2025-02-28 14:43:08.628406 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd2e65d632be5' +down_revision = '3c8bf7fb6862' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('account_details', schema=None) as batch_op: + batch_op.alter_column('amount', + existing_type=sa.INTEGER(), + type_=sa.Float(), + existing_nullable=False) + + with op.batch_alter_table('accounts', schema=None) as batch_op: + batch_op.alter_column('balance', + existing_type=sa.INTEGER(), + type_=sa.Float(), + existing_nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('accounts', schema=None) as batch_op: + batch_op.alter_column('balance', + existing_type=sa.Float(), + type_=sa.INTEGER(), + existing_nullable=False) + + with op.batch_alter_table('account_details', schema=None) as batch_op: + batch_op.alter_column('amount', + existing_type=sa.Float(), + type_=sa.INTEGER(), + existing_nullable=False) + + # ### end Alembic commands ### diff --git a/src/api/models.py b/src/api/models.py index a8bf01a802..0cdfc1ef1b 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -10,10 +10,10 @@ class User(db.Model): id: Mapped[int] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) password: Mapped[str] = mapped_column(String(80),nullable=False) - first_name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - last_name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - birthdate: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) - country: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) + first_name: Mapped[str] = mapped_column(String(120),nullable=False) + last_name: Mapped[str] = mapped_column(String(120),nullable=False) + birthdate: Mapped[str] = mapped_column(String(80),nullable=False) + country: Mapped[str] = mapped_column(String(120),nullable=False) accounts: Mapped[List["Accounts"]] = relationship() def serialize(self): @@ -32,9 +32,9 @@ class Accounts(db.Model): id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) name: Mapped[str] = mapped_column(String(80),nullable=False) - balance: Mapped[int] = mapped_column(unique=True, nullable=False) - coin: Mapped[int] = mapped_column(unique=True, nullable=False) - type: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) + balance: Mapped[float] = mapped_column(nullable=False) + coin: Mapped[str] = mapped_column(nullable=False) + type: Mapped[str] = mapped_column(nullable=False) accounts: Mapped[List["Account_details"]] = relationship() def serialize(self): @@ -52,11 +52,11 @@ class Account_details(db.Model): id: Mapped[int] = mapped_column(primary_key=True) accounts_id: Mapped[int] = mapped_column(ForeignKey("accounts.id")) detail: Mapped[str] = mapped_column(String(80),nullable=False) - amount: Mapped[int] = mapped_column(unique=True, nullable=False) - coin: Mapped[int] = mapped_column(String(120), unique=True, nullable=False) - type: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) - date: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) - time: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) + amount: Mapped[float] = mapped_column(nullable=False) + coin: Mapped[str] = mapped_column(String(120),nullable=False) + type: Mapped[str] = mapped_column(String(80),nullable=False) + date: Mapped[str] = mapped_column(String(80),nullable=False) + time: Mapped[str] = mapped_column(String(80),nullable=False) def serialize(self): return { diff --git a/src/api/routes.py b/src/api/routes.py index fa718da291..e1f40e1e6e 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -2,16 +2,14 @@ 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, Accounts from api.utils import generate_sitemap, APIException from flask_cors import CORS from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required,verify_jwt_in_request api = Blueprint('api', __name__) -# Allow CORS requests to this API CORS(api) - # @api.route('/hello', methods=['POST', 'GET']) # def handle_hello(): @@ -21,7 +19,6 @@ # return jsonify(response_body), 200 - @api.route('/users', methods=['GET']) def get_users(): @@ -37,7 +34,6 @@ def get_users(): return jsonify(response_body), 200 - @api.route('/user/', methods=['GET']) def get_one_user(user_id): try: @@ -46,8 +42,6 @@ def get_one_user(user_id): except: return jsonify({"msg":"Usuario o contraseña incorrecta"}), 404 - - @api.route("/login", methods=["POST"]) def login(): @@ -63,10 +57,36 @@ def login(): return jsonify({"msg": "this user does not exist"}), 404 +#endponts sidebar +# @api.route('/user//account', methods=['POST']) +# def post_account(user_id): +# request_body = request.json +# print(request_body) +# post_account = user_id.add_acount(request_body) +# return jsonify(request_body), 200 +@api.route('//new-account', methods=['POST']) +def post_account(user_id): + try: + request_body = request.json + exist = db.session.query(db.select(Accounts).filter_by(name=request_body["name"]).exists()).scalar() + print(exist) + if not exist: + print(exist) + print(request_body) + new_account = Accounts(user_id=user_id, name=request_body["name"], balance=request_body["balance"], coin=request_body["coin"], type=request_body["type"]) + db.session.add(new_account) + db.session.commit() + return jsonify(request_body), 200 + else: + return jsonify({"msg": "Account already exist"}), 404 + except Exception as e: + return jsonify({"msg":"Error", "error": str(e)}), 500 + + @@ -80,9 +100,20 @@ def login(): +@api.route('/accounts', methods=['GET']) +def get_accounts(): + data = db.session.scalars(db.select(Accounts)).all() + result = list(map(lambda item: item.serialize(),data)) + + if result == []: + return jsonify({"msg":"no accounts, please create one"}), 404 + response_body = { + "results": result + } + return jsonify(response_body), 200 @@ -117,47 +148,4 @@ def login(): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#espacio de laura -> \ No newline at end of file +#espacio de laura -> diff --git a/src/instance/test.db b/src/instance/test.db index 9568ea8e9d70672f29fe9ddf91913b19c945292f..014a33c54e9e729ba0076de8dc388a647aeeba02 100644 GIT binary patch delta 1094 zcmZvaPfXKL9LHbRtzG}^#|CqobJ8FZK_Za>8sZ#-%t)Lpkj<+Z&}b8+xIc$o1eFsP zs7*9_T8xR@3@nitPbMZtW4suXiNp}mY(|3zP&xYAZDbH%^KIYz{XXw~fBm&H>*kpp zwcxb_Ldb(vk40e#YP&xG=)?t8pe5v>WCQ{{<5z57ZO?6YY_hG78KqmOBDF}#%n|O2 zm7xptV{32uvMgE_msBTdre6_%m((C(rX?b?q+TbefR}O&AJ<;k+}a9Lr%`Mj?G7B= z6AR;*5b*4Y&EgmzaM#A1Clh01N@g;Bd0a{-qor90$dmRuBzXtz@eJ+MYHF2)TOeKdL!<6pi)tb4`VZ%)vipIpDcqGy;vW#vZUJ8v2oDYp?_pEK&6K>FK zl<Z9kOD!e? zb2XMRMNxw9{l>tUK_z9K6A8?DXs4&b1z(pB3ulr_=&F)TDlD#qgf#?zU=8#_-$5rw zdWnh=DUgl&ynLLAAw5j(zEuRRoo+K(K=ww}E$l z0H6Lb$}dPVq)VSFDHBaP8NE>4+h3sO+)fLw)q&;0I)LTGYA~{SabX?y-PBy>VNU!9 Dsu3$v delta 708 zcmZozz}E19WrDOII|BoQFc8B4%S0VxLv{u|?}@zpKNy7Ak2CP|^6%u?&i90GH{T?_ zOg;y$D?DvHp*$+=$GN6*g>i{<-r$_MSx~^7ow?bccXBGHmMouhkgH>et3rsQlaH%H zVsdhRXGPIU z7Nlx&Y1#_0i3e&=KFB9Axt&cw+!W0mVOv2Yc?EVMTLC1#z~pOe0_=je{3v3g>;mip zwtOgJA?yMy{7t@`le4&G#JI4z9OC4i+>Rh8L!5blTXeE2Puk=;Jd%<+3S69_ex8A$ zt_m7Zr|a{ui95@Joqm*Eh@DrT8_xd*A5CRJ>r|f5HD5sNxR43Kz2^qh|@wz(ghj5K{uf{yC%@?D<>q3R=7joU R(*zcW%`6lC*e_b3005(m#v%X! From d85f23a6d79a0b2597b684b5245e59ff7d95ab3f Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Sat, 1 Mar 2025 10:26:59 +0000 Subject: [PATCH 08/45] solucionado el problema con la base de datos --- migrations/versions/3c8bf7fb6862_.py | 38 ------------- ...cializaci\303\263n_de_la_base_de_datos.py" | 32 ++++------- migrations/versions/d2e65d632be5_.py | 50 ------------------ src/front/js/component/login-form.js | 3 +- src/front/js/pages/login.js | 2 +- src/front/js/store/flux.js | 40 +++++++++++++- src/instance/test.db | Bin 102400 -> 28672 bytes 7 files changed, 51 insertions(+), 114 deletions(-) delete mode 100644 migrations/versions/3c8bf7fb6862_.py rename "migrations/versions/f1cc659f7467_inicializaci\303\263n_de_la_base_de_datos.py" => "migrations/versions/a9589fe71e35_inicializaci\303\263n_de_la_base_de_datos.py" (68%) delete mode 100644 migrations/versions/d2e65d632be5_.py diff --git a/migrations/versions/3c8bf7fb6862_.py b/migrations/versions/3c8bf7fb6862_.py deleted file mode 100644 index ac5f574834..0000000000 --- a/migrations/versions/3c8bf7fb6862_.py +++ /dev/null @@ -1,38 +0,0 @@ -"""empty message - -Revision ID: 3c8bf7fb6862 -Revises: f1cc659f7467 -Create Date: 2025-02-28 14:38:06.815551 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '3c8bf7fb6862' -down_revision = 'f1cc659f7467' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('accounts', schema=None) as batch_op: - batch_op.alter_column('coin', - existing_type=sa.INTEGER(), - type_=sa.String(), - existing_nullable=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('accounts', schema=None) as batch_op: - batch_op.alter_column('coin', - existing_type=sa.String(), - type_=sa.INTEGER(), - existing_nullable=False) - - # ### end Alembic commands ### diff --git "a/migrations/versions/f1cc659f7467_inicializaci\303\263n_de_la_base_de_datos.py" "b/migrations/versions/a9589fe71e35_inicializaci\303\263n_de_la_base_de_datos.py" similarity index 68% rename from "migrations/versions/f1cc659f7467_inicializaci\303\263n_de_la_base_de_datos.py" rename to "migrations/versions/a9589fe71e35_inicializaci\303\263n_de_la_base_de_datos.py" index eb1132455a..b0f75ff6da 100644 --- "a/migrations/versions/f1cc659f7467_inicializaci\303\263n_de_la_base_de_datos.py" +++ "b/migrations/versions/a9589fe71e35_inicializaci\303\263n_de_la_base_de_datos.py" @@ -1,8 +1,8 @@ """Inicialización de la base de datos -Revision ID: f1cc659f7467 +Revision ID: a9589fe71e35 Revises: -Create Date: 2025-02-27 23:42:10.245977 +Create Date: 2025-03-01 10:25:26.199988 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = 'f1cc659f7467' +revision = 'a9589fe71e35' down_revision = None branch_labels = None depends_on = None @@ -27,41 +27,29 @@ def upgrade(): sa.Column('birthdate', sa.String(length=80), nullable=False), sa.Column('country', sa.String(length=120), nullable=False), sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('birthdate'), - sa.UniqueConstraint('country'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('first_name'), - sa.UniqueConstraint('last_name') + sa.UniqueConstraint('email') ) op.create_table('accounts', sa.Column('id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=80), nullable=False), - sa.Column('balance', sa.Integer(), nullable=False), - sa.Column('coin', sa.Integer(), nullable=False), - sa.Column('type', sa.String(length=80), nullable=False), + sa.Column('balance', sa.Float(), nullable=False), + sa.Column('coin', sa.String(), nullable=False), + sa.Column('type', sa.String(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('balance'), - sa.UniqueConstraint('coin'), - sa.UniqueConstraint('type') + sa.PrimaryKeyConstraint('id') ) op.create_table('account_details', sa.Column('id', sa.Integer(), nullable=False), sa.Column('accounts_id', sa.Integer(), nullable=False), sa.Column('detail', sa.String(length=80), nullable=False), - sa.Column('amount', sa.Integer(), nullable=False), + sa.Column('amount', sa.Float(), nullable=False), sa.Column('coin', sa.String(length=120), nullable=False), sa.Column('type', sa.String(length=80), nullable=False), sa.Column('date', sa.String(length=80), nullable=False), sa.Column('time', sa.String(length=80), nullable=False), sa.ForeignKeyConstraint(['accounts_id'], ['accounts.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('amount'), - sa.UniqueConstraint('coin'), - sa.UniqueConstraint('date'), - sa.UniqueConstraint('time'), - sa.UniqueConstraint('type') + sa.PrimaryKeyConstraint('id') ) # ### end Alembic commands ### diff --git a/migrations/versions/d2e65d632be5_.py b/migrations/versions/d2e65d632be5_.py deleted file mode 100644 index 8c8ea90c95..0000000000 --- a/migrations/versions/d2e65d632be5_.py +++ /dev/null @@ -1,50 +0,0 @@ -"""empty message - -Revision ID: d2e65d632be5 -Revises: 3c8bf7fb6862 -Create Date: 2025-02-28 14:43:08.628406 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'd2e65d632be5' -down_revision = '3c8bf7fb6862' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('account_details', schema=None) as batch_op: - batch_op.alter_column('amount', - existing_type=sa.INTEGER(), - type_=sa.Float(), - existing_nullable=False) - - with op.batch_alter_table('accounts', schema=None) as batch_op: - batch_op.alter_column('balance', - existing_type=sa.INTEGER(), - type_=sa.Float(), - existing_nullable=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('accounts', schema=None) as batch_op: - batch_op.alter_column('balance', - existing_type=sa.Float(), - type_=sa.INTEGER(), - existing_nullable=False) - - with op.batch_alter_table('account_details', schema=None) as batch_op: - batch_op.alter_column('amount', - existing_type=sa.Float(), - type_=sa.INTEGER(), - existing_nullable=False) - - # ### end Alembic commands ### diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js index a50c819d58..1fd1e769ac 100644 --- a/src/front/js/component/login-form.js +++ b/src/front/js/component/login-form.js @@ -10,9 +10,10 @@ export const LoginForm = () => { async function handleSubmit(e) { e.preventDefault() - // actions.login(email, password); + actions.login(email, password); console.log(email); console.log(password); + navigate("/principal-page") } return ( diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js index e03a231627..fff3fb0855 100644 --- a/src/front/js/pages/login.js +++ b/src/front/js/pages/login.js @@ -11,7 +11,7 @@ export const Login = () => {

OPTIMA

"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

-
+
diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index cc56951a22..92e1f2ec10 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -13,7 +13,8 @@ const getState = ({ getStore, getActions, setStore }) => { background: "white", initial: "white" } - ] + ], + logged: false }, actions: { // Use getActions to call a function within a fuction @@ -46,7 +47,42 @@ const getState = ({ getStore, getActions, setStore }) => { //reset the global store setStore({ demo: demo }); - } + }, + login: async (email, password) => { + const myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); + + const raw = JSON.stringify({ + "email": email, + "password": password + }); + + const requestOptions = { + method: "POST", + headers: myHeaders, + body: raw, + redirect: "follow" + }; + + try { + const response = await fetch("https://musical-space-disco-9vqj5g5jpv92p7xp-3001.app.github.dev/api/login", requestOptions); + const result = await response.json(); + if (response.status === 200) { + // localStorage.setItem("token", result.access_token) + // getActions().verifyToken() + // getActions().getPrivate() + setStore({ logged: true }) + console.log("hola mundo"); + + } else if (response.status === 404 || response.status === 401) { + setStore({ logged: false }) + console.log("adios mundo"); + } + } catch (error) { + console.error(error); + return false; + }; + }, } }; }; diff --git a/src/instance/test.db b/src/instance/test.db index 014a33c54e9e729ba0076de8dc388a647aeeba02..43a905571ca22cc7d558e1264bf40c32428a7048 100644 GIT binary patch delta 410 zcmZozz}E19ae}lU2Ll5GI}pPF%S0VxNe%`*?}@zpKN#4!RT=ns`FHYe;(Nj~k6U%K zpuk!#uBI|Jc5!WO##ZIYIh@L}?9M^1jv=lJA&yQyt_q3C$@!&uCGjb#C5f3i#giZK zNKZb=`Ec@XE<+wJ%_c!sc5zu*#^%z=h1{ZIY}m9y^z7uWm@L7=J9!$H$mB2{O^{xF zW_EE=QN|X>%?EiD7`ak&ffi_TX{xhK-pi-BnVtWw0C%FLsfA@)s<~mR@#g>XU-@~E z1WXrAP?&UqjiZEt|1$p){t}>P()jE3nAsUPjhW=-of7ks^A%E36&!PbCg)`)CMK#s yNy*PiEJ`&hPE5?mFDl9h(z;AQ^@dp0V^glp2vn}mBrl6$JVZ4wis_3c2mk?NK delta 1610 zcmZvbPi)&%9LJyS){Y$~zhrBcnBc|g{)EzL$&&sVgRFn19459Q(0Y!X)Y~kQxU(IJ zUSMIsp_euI1Shl$y_QHIG$c46Atb~FA(gF~CI)JeP?1;%J0QV(c8qGTFa9~s*Pq|} zec$iB+;~9U_=sK2CIKNN;W&tc#S!bBC|3ZTe7P6nG<1N~5yaqg@w50Z@jLPN<9d9S zuXD%QEq0C7`6JP*5uV%NK8~F6KErFnYwO(a2IKQ*-`*9`ZB&Di%a1&~Lr&uXN>E;^z5~o5_$H>E^LqDOMVx!0<5m#fO42HK+Em+(pf>PxlV!McDn-3(^uLHHlvdYG z``S~hanG2!9GFyV;R6>?uIQ#^*V<}mEfbZ?4+qr}QQ7>Eul8y9GZ9@ZX;YJnQ{_@!>;Id344FN`WDLGW6s#avft7wT z@g|%-O-InUg!e;Q!nDE1gD2gNcL8I2E!;Qbzty89WI1oKdiYUk$>Ps&m}k;nCgCFl6X3D$W83wS zA(s7R5-dtEJZ=}2+p~#JX4ypK7J~cmDcr&dU3lvl!y!uMq%1wJFL#!8+nD+312R5b z(k#oc8iuL0ZS6SiV(FZR8HO?QL)je|b173NoI+tv;!@IpoB7t8tlBY+xod`L8Ul91 zz$SvfU=xVz@1YcBvecju6Jv7Z*_)2$8HwMUcNf7f-k`f)FGYr Date: Sat, 1 Mar 2025 13:09:10 +0000 Subject: [PATCH 09/45] sidebar casi terminado , consumo de api de terceros y nombre programatico --- src/front/js/component/sidebar.js | 47 ++++++++++++++++++++++--------- src/front/js/layout.js | 2 +- src/front/styles/sidebar.css | 26 ++++++----------- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js index cf2377ef88..339e511af1 100644 --- a/src/front/js/component/sidebar.js +++ b/src/front/js/component/sidebar.js @@ -1,13 +1,39 @@ import React from "react"; import { Link } from "react-router-dom"; import "/src/front/styles/sidebar.css"; +import { useState } from "react"; +import { useEffect } from "react"; export const Sidebar = () => { + const [userName, setUserName] = useState("") + const [userLastName, setLastUserName] = useState("") + async function getUser() { + const myHeaders = new Headers(); + myHeaders.append("Cookie", ".Tunnels.Relay.WebForwarding.Cookies=CfDJ8Cs4yarcs6pKkdu0hlKHsZsY5nM0Bh-G8KdaZucRe2wc-P0OBfWNR4e_Ry-wYVh6y7msxvQ0bt_A84DgHodrU_OCCrKzu1cgD7lBCg0Y9XtWdLuWjQ5bDQEa6nI-aPNG721L5Swl3R7MTaWyc63l2Gfva0VAPJd5aTjfuIPHuEUwqsm5SBixx1f0LbKQSz3YRcuUinDNLwoMSypZCqgCRKw0vXaf_WPMxJXnwem-NNI6X2zDt3Ts0S7w44nPpfiSZgnkudY_gI48iqzvAxpwzKCWfCgP__g__IUpKfqJ96iiVcw05AtVf6MOhmmTS4q5bIt__wa4FeBk_GBHEspoCt-Rf_W2PmhdrGi7dbasxNmRqTjrhWf-1U6g1WoxuvUZrqwJ_D0u-NS5wFmDVQRY0JZjgUv-HnMddgQ32FlS6l8w9Q5l9KmMt7QliLxjvXlWcPbmiIhIrcDuPlr9NKFakMg9CFX-kuVToRFwszOO7kPJn5QbAeeYdxsx5OSAL2lTO2haHjRoHaAaLdfGBIDmOdKeiymLmcrw7ro0RER4FBmm9G-kNPIYGhYgI4NxpK9stDaRD4h49usa6GGyPzBN-iyXVh-Cz6w-QpoaauccXovZnnPSZcSLW-VO4tEiLkAiqgFoLq2tvwTam5z5ZCqfSwmxvYa87f10Fhd-kX-4thA_nLPhaXA4fC7cY0V2WvemkDP4MiykRK9x4RhI4BzIg3bxy1fX9bBQ7gggODLTokhOHSfPJ_JG9u-3YjgaeLdtOBby5h5Jk8AvpXBDG4bZNQAvhdvCfeOVdIW-SVLxZrypB8wh76WzWFDGsiZRXCnXtilF0VkPULs1VLOChY-cSAlrzpgFCA2OAwmY5QczvAn7oAMcf8_Rsc82Wg1NrsgmFqIoagVgUIaYsTaKMLvwnTKf06Q4jz01sGRqbDy_9c-QQ1v_OQjT2xYTbYjPsituCX0UUSeaYPEVyDEsOo1KAzM3mqPSSoJfLfYHPuf-26Wp"); + + const requestOptions = { + method: "GET", + headers: myHeaders, + redirect: "follow" + }; + + try { + const response = await fetch("https://ubiquitous-parakeet-wqxj59w67w7h9p5j-3001.app.github.dev/api/user/2", requestOptions); + const result = await response.json(); + setUserName(result.result.first_name.toUpperCase()) + setLastUserName(result.result.last_name.toUpperCase()) + } catch (error) { + console.error(error); + }; + } + useEffect(() => { + getUser() + }, []) return (
- + Optima logo @@ -17,35 +43,30 @@ export const Sidebar = () => {

slogan

-
- User -

@usuario-db

+
+ +

{userName} {userLastName}

-
  • - + Cuentas
  • - + Movimientos
  • - -
    - -
  • - + FAQs
  • - + Sign Out
  • diff --git a/src/front/js/layout.js b/src/front/js/layout.js index 10ae67f8c1..255675c128 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -26,7 +26,7 @@ const Layout = () => { } path="/" /> } path="/demo" /> - } path="/principal-page" /> + } path="/cuentas" /> } path="/single/:theid" /> Not found!} /> diff --git a/src/front/styles/sidebar.css b/src/front/styles/sidebar.css index 4b0bf77822..a777fcbc73 100644 --- a/src/front/styles/sidebar.css +++ b/src/front/styles/sidebar.css @@ -7,7 +7,7 @@ padding: 20px; } -/* Links del sidebar */ + .sidebar .nav-link { color: rgba(255, 255, 255, 0.75); transition: color 0.3s ease; @@ -26,24 +26,16 @@ margin-right: 8px; } - -.sidebar .dropdown-toggle { - display: flex; - align-items: center; +.avatar{ + border-radius:100%; } -.sidebar .dropdown-toggle img { - border-radius: 50%; -} -.sidebar .dropdown-menu { - background-color: #212529; -} -.sidebar .dropdown-menu .dropdown-item { - color: white; -} -.sidebar .dropdown-menu .dropdown-item:hover { - background-color: rgba(255, 255, 255, 0.1); -} + + + + + + From cdf7d4fdcaac0fd32dbd9c596ad6c049a4c749b1 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:54:14 +0000 Subject: [PATCH 10/45] =?UTF-8?q?correcci=C3=B3n=20del=20consumo=20de=20lo?= =?UTF-8?q?s=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/component/sidebar.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js index 339e511af1..b2788e5594 100644 --- a/src/front/js/component/sidebar.js +++ b/src/front/js/component/sidebar.js @@ -19,8 +19,11 @@ export const Sidebar = () => { }; try { - const response = await fetch("https://ubiquitous-parakeet-wqxj59w67w7h9p5j-3001.app.github.dev/api/user/2", requestOptions); + const response = await fetch(`${process.env.BACKEND_URL}/api/user/1`, requestOptions); const result = await response.json(); + console.log(result); + + setUserName(result.result.first_name.toUpperCase()) setLastUserName(result.result.last_name.toUpperCase()) } catch (error) { From 0e77f9e4d696e066c9446abfd8ddc12ef8e15702 Mon Sep 17 00:00:00 2001 From: Luis Malizia Date: Sat, 1 Mar 2025 17:17:49 +0000 Subject: [PATCH 11/45] se creo el endpoint de login(token) y el veryfy token --- src/api/routes.py | 81 ++++++++++++----------------------------------- src/app.py | 5 +++ 2 files changed, 25 insertions(+), 61 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index e1f40e1e6e..0c33dec7e1 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -6,6 +6,8 @@ from api.utils import generate_sitemap, APIException from flask_cors import CORS from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required,verify_jwt_in_request +from flask_jwt_extended.exceptions import NoAuthorizationError + api = Blueprint('api', __name__) CORS(api) @@ -27,11 +29,10 @@ def get_users(): if result == []: return jsonify({"msg":"Usuario no encontrado"}), 404 - + response_body = { "results": result } - return jsonify(response_body), 200 @api.route('/user/', methods=['GET']) @@ -51,20 +52,18 @@ def login(): user = db.session.execute(db.select(User).filter_by(email=email)).scalar_one() if password != user.password: return jsonify({"msg": "Bad email or password"}), 401 - # access_token = create_access_token(identity=email) - return jsonify({"msg": "Succesfull access"}) + access_token = create_access_token(identity=email) + return jsonify(access_token=access_token) except: return jsonify({"msg": "this user does not exist"}), 404 - -#endponts sidebar -# @api.route('/user//account', methods=['POST']) -# def post_account(user_id): -# request_body = request.json -# print(request_body) -# post_account = user_id.add_acount(request_body) -# return jsonify(request_body), 200 - +@api.route("/protected", methods=["GET"]) +@jwt_required() +def protected(): + # Access the identity of the current user with get_jwt_identity + current_user = get_jwt_identity() + return jsonify(logged_in_as=current_user), 200 + @api.route('//new-account', methods=['POST']) def post_account(user_id): try: @@ -86,20 +85,6 @@ def post_account(user_id): return jsonify({"msg":"Error", "error": str(e)}), 500 - - - - - - - - - - - - - - @api.route('/accounts', methods=['GET']) def get_accounts(): @@ -115,37 +100,11 @@ def get_accounts(): return jsonify(response_body), 200 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#espacio de laura -> +@api.route("/verify-token", methods=["GET"]) +def verify_token(): + try: + verify_jwt_in_request() # Verifica la validez del token + identity = get_jwt_identity() # Obtiene el usuario del token + return jsonify({"valid": True, "user": identity}), 200 + except NoAuthorizationError: + return jsonify({"valid": False, "message": "Token inválido o no proporcionado"}), 401 \ No newline at end of file diff --git a/src/app.py b/src/app.py index 0ea8351d5f..ee9ad53fec 100644 --- a/src/app.py +++ b/src/app.py @@ -10,6 +10,7 @@ from api.routes import api from api.admin import setup_admin from api.commands import setup_commands +from flask_jwt_extended import JWTManager # from models import Person @@ -31,6 +32,10 @@ MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) +# Setup the Flask-JWT-Extended extension +app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! +jwt = JWTManager(app) + # add the admin setup_admin(app) From 23c31959634abbfcf1d656ebc480398e7f86dd26 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Sat, 1 Mar 2025 17:34:10 +0000 Subject: [PATCH 12/45] creacion card --- src/front/js/component/card.js | 13 +++++++++++++ src/front/js/pages/principalPage.js | 10 +++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/front/js/component/card.js diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js new file mode 100644 index 0000000000..375a304e31 --- /dev/null +++ b/src/front/js/component/card.js @@ -0,0 +1,13 @@ +import React from "react"; + +export const Card = () => { + return ( +
    +
    +
    Card title
    +

    With supporting text below as a natural lead-in to additional content.

    + Button +
    +
    + ) +} \ No newline at end of file diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 0565e462f2..953c5fbf38 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -1,8 +1,16 @@ import React from "react"; import { Sidebar } from "../component/sidebar"; +import { Card } from "../component/card"; export const PrincipalPage = () => { return ( - + <> +
    + +
    + +
    +
    + ) } \ No newline at end of file From 00ed84565b0e1e8c5f6fec117fbcfd30a8961398 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:21:47 +0000 Subject: [PATCH 13/45] =?UTF-8?q?se=20termin=C3=B3=20con=20el=20maquetado?= =?UTF-8?q?=20de=20la=20vista=20CUENTAS,=20faltar=C3=ADa=20linkear=20las?= =?UTF-8?q?=20dem=C3=A1s=20vistas,=20se=20est=C3=A1=20a=20la=20espera=20de?= =?UTF-8?q?=20los=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/component/card.js | 2 +- src/front/js/pages/principalPage.js | 19 +++++++++++++------ src/front/styles/container.css | 15 +++++++++++++++ src/front/styles/sidebar.css | 2 +- 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 src/front/styles/container.css diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js index 375a304e31..da822118b3 100644 --- a/src/front/js/component/card.js +++ b/src/front/js/component/card.js @@ -2,7 +2,7 @@ import React from "react"; export const Card = () => { return ( -
    +
    Card title

    With supporting text below as a natural lead-in to additional content.

    diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 953c5fbf38..6748d249dd 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -1,16 +1,23 @@ import React from "react"; import { Sidebar } from "../component/sidebar"; import { Card } from "../component/card"; +import "../../styles/container.css"; export const PrincipalPage = () => { return ( - <> -
    - -
    - +
    + +
    +
    +

    Balance general

    +
    + + + + +
    - +
    ) } \ No newline at end of file diff --git a/src/front/styles/container.css b/src/front/styles/container.css new file mode 100644 index 0000000000..937cd1b521 --- /dev/null +++ b/src/front/styles/container.css @@ -0,0 +1,15 @@ +.scrollmenu { + display: flex; + overflow-y: auto; + flex-direction: column; + max-height: 80vh; +} +.card{ + min-width: 18rem; + min-height: 10rem; +} +.row{ + padding: 2rem; + background-color:antiquewhite; + height: 100vh; +} \ No newline at end of file diff --git a/src/front/styles/sidebar.css b/src/front/styles/sidebar.css index a777fcbc73..84da545aa0 100644 --- a/src/front/styles/sidebar.css +++ b/src/front/styles/sidebar.css @@ -1,6 +1,6 @@ .sidebar { - width: 280px; + min-width: 20vw; height: 100vh; background-color: #343a40; color: white; From c762520139593aa82df8cb4f2f357e3f58ca3718 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Sun, 2 Mar 2025 10:48:35 +0000 Subject: [PATCH 14/45] adelanto de estilos de card --- src/front/js/component/card.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js index da822118b3..dcbd1c4b2c 100644 --- a/src/front/js/component/card.js +++ b/src/front/js/component/card.js @@ -2,12 +2,22 @@ import React from "react"; export const Card = () => { return ( -
    -
    -
    Card title
    -

    With supporting text below as a natural lead-in to additional content.

    - Button +
    +
    +
    Nombre Cuenta
    +
    +
    +

    Saldo

    + + +
    +

    Moneda

    +
    +
    + Button +
    +
    ) } \ No newline at end of file From 0cf26d8c312b955667a150bb69832e479680c1ee Mon Sep 17 00:00:00 2001 From: Luis Malizia Date: Sun, 2 Mar 2025 11:26:31 +0000 Subject: [PATCH 15/45] se crearon los endpoinst para mostrar todas la cuentas y una cuenta especifica de un usuario especifico --- src/api/routes.py | 57 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index 0c33dec7e1..06569dbcb6 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -8,10 +8,8 @@ from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required,verify_jwt_in_request from flask_jwt_extended.exceptions import NoAuthorizationError - api = Blueprint('api', __name__) CORS(api) - # @api.route('/hello', methods=['POST', 'GET']) # def handle_hello(): @@ -20,21 +18,18 @@ # } # return jsonify(response_body), 200 - + #este endpoint busca y muestra a todos los usuarios registrados @api.route('/users', methods=['GET']) def get_users(): - data = db.session.scalars(db.select(User)).all() result = list(map(lambda item: item.serialize(),data)) - if result == []: return jsonify({"msg":"Usuario no encontrado"}), 404 - response_body = { "results": result } return jsonify(response_body), 200 - + #este endpoint busca un usuario especifico entre todos los demas @api.route('/user/', methods=['GET']) def get_one_user(user_id): try: @@ -42,10 +37,9 @@ def get_one_user(user_id): return jsonify({"result":user.serialize()}), 200 except: return jsonify({"msg":"Usuario o contraseña incorrecta"}), 404 - + #este endpoint valida los datos de usuario y crea el token de acceso @api.route("/login", methods=["POST"]) def login(): - email = request.json.get("email", None) password = request.json.get("password", None) try: @@ -56,55 +50,60 @@ def login(): return jsonify(access_token=access_token) except: return jsonify({"msg": "this user does not exist"}), 404 - + #este endpoin protege la ruta del usuario @api.route("/protected", methods=["GET"]) @jwt_required() def protected(): # Access the identity of the current user with get_jwt_identity current_user = get_jwt_identity() return jsonify(logged_in_as=current_user), 200 - + #este endpoint crea una cuenta con el id del usuario @api.route('//new-account', methods=['POST']) def post_account(user_id): try: request_body = request.json exist = db.session.query(db.select(Accounts).filter_by(name=request_body["name"]).exists()).scalar() - print(exist) if not exist: - print(exist) - print(request_body) new_account = Accounts(user_id=user_id, name=request_body["name"], balance=request_body["balance"], coin=request_body["coin"], type=request_body["type"]) db.session.add(new_account) db.session.commit() return jsonify(request_body), 200 - else: return jsonify({"msg": "Account already exist"}), 404 except Exception as e: return jsonify({"msg":"Error", "error": str(e)}), 500 - - + #este endpint muestra todas cuentas en general @api.route('/accounts', methods=['GET']) def get_accounts(): - data = db.session.scalars(db.select(Accounts)).all() result = list(map(lambda item: item.serialize(),data)) - if result == []: return jsonify({"msg":"no accounts, please create one"}), 404 - response_body = { "results": result } - return jsonify(response_body), 200 - -@api.route("/verify-token", methods=["GET"]) -def verify_token(): + # este endpoin busca la lista de cuentas y muestra una sola cuenta especifica +@api.route('/accounts/', methods=['GET']) +def get_one_accounts(accounts_id): try: - verify_jwt_in_request() # Verifica la validez del token - identity = get_jwt_identity() # Obtiene el usuario del token - return jsonify({"valid": True, "user": identity}), 200 - except NoAuthorizationError: - return jsonify({"valid": False, "message": "Token inválido o no proporcionado"}), 401 \ No newline at end of file + account = db.session.execute(db.select(Accounts).filter_by(id=accounts_id)).scalar_one() + return jsonify({"result":account.serialize()}), 200 + except: + return jsonify({"msg":"account not found"}), 404 + #este endpoint valida si existe el usuario y muestra las cuentas de un usuario especifico +@api.route('/user//accounts', methods=['GET']) +def get_one_account_to_one_user(user_id): + try: + exist = db.session.query(db.select(Accounts).filter_by(user_id=user_id).exists()).scalar() + if exist: + accounts = db.session.execute(db.select(Accounts).filter_by(user_id=user_id)).scalars().all() + if accounts != []: + return jsonify({"result": [acc.serialize() for acc in accounts]}), 200 + return jsonify({"msg": "No accounts to show"}) + else: + return jsonify({"msg": "user doesn't exist"}), 404 + + except Exception as e: + return jsonify({"msg":"Error", "error": str(e)}), 500 \ No newline at end of file From 15fe0d0e971bd1a75dda6b82df2be63fa18dfa1b Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Sun, 2 Mar 2025 11:56:13 +0000 Subject: [PATCH 16/45] =?UTF-8?q?se=20termin=C3=B3=20de=20validar=20el=20l?= =?UTF-8?q?ogin,=20con=20cambios=20de=20estilo=20en=20caso=20de=20contrase?= =?UTF-8?q?=C3=B1as=20erradas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/component/login-form.js | 19 +++++-- src/front/js/component/sidebar.js | 15 +++-- src/front/js/pages/principalPage.js | 2 +- src/front/js/store/flux.js | 82 ++++++++++++++------------- src/front/styles/login.css | 4 ++ src/front/styles/sidebar.css | 4 +- src/instance/test.db | Bin 28672 -> 28672 bytes 7 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js index 072547a0b4..24c6c3ee5a 100644 --- a/src/front/js/component/login-form.js +++ b/src/front/js/component/login-form.js @@ -3,6 +3,7 @@ import { Context } from "../store/appContext"; import { Link, useNavigate } from "react-router-dom"; export const LoginForm = () => { + const [invalidAccount, setInvalidAccount] = useState(false) const [email, setEmail] = useState("") const [password, setPassword] = useState("") const { store, actions } = useContext(Context) @@ -11,12 +12,19 @@ export const LoginForm = () => { async function handleSubmit(e) { e.preventDefault() actions.login(email, password); - } - useEffect(() => { + await actions.login(email, password) if (!store.logged) { - navigate("/") + setInvalidAccount(true) } else { - navigate("/cuentas") + setInvalidAccount(false) + } + } + useEffect(() => { + if (store.logged) { + navigate("/cuentas"); + } + if (invalidAccount) { + setInvalidAccount(false) } }, [store.logged]) @@ -25,7 +33,8 @@ export const LoginForm = () => {
    setEmail(e.target.value)} value={email} /> -
    We'll never share your email with anyone else.
    + {!invalidAccount ?
    Nunca compartiremos su correo electrónico con nadie más.
    + :
    Correo o Contraseña errada
    }
    diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js index b2788e5594..ecb4df2236 100644 --- a/src/front/js/component/sidebar.js +++ b/src/front/js/component/sidebar.js @@ -1,13 +1,18 @@ -import React from "react"; +import React, { useEffect, useContext,useState } from "react"; +import { Context } from "../store/appContext"; import { Link } from "react-router-dom"; import "/src/front/styles/sidebar.css"; -import { useState } from "react"; -import { useEffect } from "react"; export const Sidebar = () => { const [userName, setUserName] = useState("") const [userLastName, setLastUserName] = useState("") + const { store, actions } = useContext(Context) + + const handleClick = () => { + actions.logout() + } + async function getUser() { const myHeaders = new Headers(); myHeaders.append("Cookie", ".Tunnels.Relay.WebForwarding.Cookies=CfDJ8Cs4yarcs6pKkdu0hlKHsZsY5nM0Bh-G8KdaZucRe2wc-P0OBfWNR4e_Ry-wYVh6y7msxvQ0bt_A84DgHodrU_OCCrKzu1cgD7lBCg0Y9XtWdLuWjQ5bDQEa6nI-aPNG721L5Swl3R7MTaWyc63l2Gfva0VAPJd5aTjfuIPHuEUwqsm5SBixx1f0LbKQSz3YRcuUinDNLwoMSypZCqgCRKw0vXaf_WPMxJXnwem-NNI6X2zDt3Ts0S7w44nPpfiSZgnkudY_gI48iqzvAxpwzKCWfCgP__g__IUpKfqJ96iiVcw05AtVf6MOhmmTS4q5bIt__wa4FeBk_GBHEspoCt-Rf_W2PmhdrGi7dbasxNmRqTjrhWf-1U6g1WoxuvUZrqwJ_D0u-NS5wFmDVQRY0JZjgUv-HnMddgQ32FlS6l8w9Q5l9KmMt7QliLxjvXlWcPbmiIhIrcDuPlr9NKFakMg9CFX-kuVToRFwszOO7kPJn5QbAeeYdxsx5OSAL2lTO2haHjRoHaAaLdfGBIDmOdKeiymLmcrw7ro0RER4FBmm9G-kNPIYGhYgI4NxpK9stDaRD4h49usa6GGyPzBN-iyXVh-Cz6w-QpoaauccXovZnnPSZcSLW-VO4tEiLkAiqgFoLq2tvwTam5z5ZCqfSwmxvYa87f10Fhd-kX-4thA_nLPhaXA4fC7cY0V2WvemkDP4MiykRK9x4RhI4BzIg3bxy1fX9bBQ7gggODLTokhOHSfPJ_JG9u-3YjgaeLdtOBby5h5Jk8AvpXBDG4bZNQAvhdvCfeOVdIW-SVLxZrypB8wh76WzWFDGsiZRXCnXtilF0VkPULs1VLOChY-cSAlrzpgFCA2OAwmY5QczvAn7oAMcf8_Rsc82Wg1NrsgmFqIoagVgUIaYsTaKMLvwnTKf06Q4jz01sGRqbDy_9c-QQ1v_OQjT2xYTbYjPsituCX0UUSeaYPEVyDEsOo1KAzM3mqPSSoJfLfYHPuf-26Wp"); @@ -69,8 +74,8 @@ export const Sidebar = () => {
  • - - Sign Out + + Logout
diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 6748d249dd..ca100ff101 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -15,7 +15,7 @@ export const PrincipalPage = () => { -
+
diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index 6742eade3d..600b9026e6 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -15,7 +15,8 @@ const getState = ({ getStore, getActions, setStore }) => { } ], logged: false, - user:"" + user:"", + auth:false }, actions: { // Use getActions to call a function within a fuction @@ -66,55 +67,58 @@ const getState = ({ getStore, getActions, setStore }) => { }; try { - const response = await fetch("https://musical-space-disco-9vqj5g5jpv92p7xp-3001.app.github.dev/api/login", requestOptions); + const response = await fetch(`${process.env.BACKEND_URL}/api/login`, requestOptions); const result = await response.json(); if (response.status === 200) { - // localStorage.setItem("token", result.access_token) - // getActions().verifyToken() - // getActions().getPrivate() - setStore({ logged: true }) - console.log("hola mundo"); - + localStorage.setItem("token", result.access_token) + setStore({ logged: true }) + getActions().verifyToken() + getActions().getPrivate() } else if (response.status === 404 || response.status === 401) { setStore({ logged: false }) - console.log("adios mundo"); } } catch (error) { console.error(error); return false; }; }, - // getPrivate: async () => { - // let token = localStorage.getItem("token") - // try { - // const response = await fetch("https://stunning-memory-pvgpw5wpvq6244-3001.app.github.dev/api/private", { - // method: "GET", - // headers: { - // "Authorization": `Bearer ${token}` - // }, - // }); - // const result = await response.json(); - // setStore({ user: result.logged_in_as }) + getPrivate: async () => { + let token = localStorage.getItem("token") + try { + const response = await fetch(`${process.env.BACKEND_URL}/api/private`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + }, + }); + const result = await response.json(); + setStore({ user: result.logged_in_as }) - // } catch (error) { - // console.error(error); - // }; - // }, - // verifyToken: async () => { - // let token = localStorage.getItem("token") - // try { - // const response = await fetch("https://stunning-memory-pvgpw5wpvq6244-3001.app.github.dev/api/verify-token", { - // method: "GET", - // headers: { - // "Authorization": `Bearer ${token}` - // }, - // }); - // const result = await response.json(); - // setStore({ auth: result.valid }) - // } catch (error) { - // console.error(error); - // }; - // }, + } catch (error) { + console.error(error); + }; + }, + verifyToken: async () => { + let token = localStorage.getItem("token") + try { + const response = await fetch(`${process.env.BACKEND_URL}/api/verify-token`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + }, + }); + const result = await response.json(); + setStore({ auth: result.valid }) + } catch (error) { + console.error(error); + }; + }, + logout: () => { + //borrar el token del localStorage + localStorage.removeItem("token") + setStore({ logged: false }) + getActions().verifyToken() + }, } }; }; diff --git a/src/front/styles/login.css b/src/front/styles/login.css index 9b87e8396e..05679cb7a8 100644 --- a/src/front/styles/login.css +++ b/src/front/styles/login.css @@ -14,4 +14,8 @@ display: flex; flex-direction: column; text-align: center; +} +.invalidAccount{ + color: red; + font-weight: bold; } \ No newline at end of file diff --git a/src/front/styles/sidebar.css b/src/front/styles/sidebar.css index 84da545aa0..3d7346b4e0 100644 --- a/src/front/styles/sidebar.css +++ b/src/front/styles/sidebar.css @@ -29,7 +29,9 @@ .avatar{ border-radius:100%; } - +.logout{ + color: red; +} diff --git a/src/instance/test.db b/src/instance/test.db index 4b5e195526a7bbea1e83270751aea5189b35aa70..7b410e4579000d8a43661d1e52f6fa5474d918fe 100644 GIT binary patch delta 134 zcmZp8z}WDBae_1>_e2?IM(&LXOY}LI_@^-NpXQ&kSx}&Z-`tdmgF#nZl3!6eIWadg zC*L7GH!(9uFF8Ng(8$=t)EO-3Y+$GlM241@2Cl^giHA2PPCjjK2{h>j1OE;Fw?LE5 T@QaEt>oTI7!L<3Uzpw%TWHBiu delta 52 zcmZp8z}WDBae_1>*F+g-My`zsOZ3?o`A;+OpWZAeaFBoUX?sf`|1AUm+s%RrkNG#h I^%qtE0LM8I!~g&Q From 11da59dbcdec52bdce149ec84774ffb7af7c5b2c Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Sun, 2 Mar 2025 11:58:56 +0000 Subject: [PATCH 17/45] =?UTF-8?q?refactorizaci=C3=B3n=20de=20c=C3=B3digo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/component/login-form.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js index 24c6c3ee5a..d2d14c6ff3 100644 --- a/src/front/js/component/login-form.js +++ b/src/front/js/component/login-form.js @@ -11,7 +11,6 @@ export const LoginForm = () => { async function handleSubmit(e) { e.preventDefault() - actions.login(email, password); await actions.login(email, password) if (!store.logged) { setInvalidAccount(true) From 9e10af43359934be00aee8afc679b40b4ada330f Mon Sep 17 00:00:00 2001 From: LauraPostigo <166739141+LauraPostigo@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:02:42 +0000 Subject: [PATCH 18/45] endpoint registro usuario creado --- src/api/routes.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/api/routes.py b/src/api/routes.py index 06569dbcb6..9abdfdec9f 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -106,4 +106,18 @@ def get_one_account_to_one_user(user_id): return jsonify({"msg": "user doesn't exist"}), 404 except Exception as e: - return jsonify({"msg":"Error", "error": str(e)}), 500 \ No newline at end of file + return jsonify({"msg":"Error", "error": str(e)}), 500 + +# endpoints +# registro usuario +@api.route("/signup", methods=["POST"]) +def signup(): + body = request.json + # para manejo de errores poner exactamente el nombre del front igual en los campos entre parentesis (esperar a que se haga el front) + if not body or not body.get("email") or not body.get("password") or not body.get("lastname")or not body.get("firstname")or not body.get("birthday")or not body.get("country"): + return jsonify({"msg": "missing fields"}), 400 + # encajar con los nombres del front estos (solo los que estan entre comillas) + new_user = User(email = body["email"],password= body["password"],last_name= body["lastname"],first_name= body["firstname"],birthday= body["birthday"],country= body["country"]) + db.session.add(new_user) + db.session.commit() + return jsonify({"msg": "user created"}), 201 \ No newline at end of file From b031e674b124d8f9d9b676281ffd5df6aa4ce8e6 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:14:55 +0000 Subject: [PATCH 19/45] =?UTF-8?q?se=20agreg=C3=B3=20el=20bcrypt=20para=20e?= =?UTF-8?q?ncriptar=20contrase=C3=B1a=20y=20se=20ajsutaron=20estilos=20del?= =?UTF-8?q?=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pipfile | 1 + Pipfile.lock | 67 ++++++++++++++++++++++++++- src/api/routes.py | 8 +++- src/app.py | 2 + src/front/js/component/login-form.js | 19 +++++--- src/front/js/component/sidebar.js | 5 +- src/front/js/store/flux.js | 10 ++-- src/front/styles/login.css | 26 +++++++++++ src/instance/test.db | Bin 28672 -> 28672 bytes 9 files changed, 119 insertions(+), 19 deletions(-) diff --git a/Pipfile b/Pipfile index cea82ef827..2dfb92dd71 100644 --- a/Pipfile +++ b/Pipfile @@ -21,6 +21,7 @@ flask-admin = "*" typing-extensions = "*" flask-jwt-extended = "*" wtforms = "==3.1.2" +flask-bcrypt = "*" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index 1a42a158f3..970f0382ff 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9d8d7f2c4ed85902b3f7b55f989c7228ecdf6b24ae33f6eee42c006638aeed5e" + "sha256": "24a5225f4f4832d2d6b84005467001959f4697779cedf523469c3b11d4cdc508" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,63 @@ "markers": "python_version >= '3.8'", "version": "==1.14.1" }, + "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" + ], + "markers": "python_version >= '3.8'", + "version": "==4.3.0" + }, "blinker": { "hashes": [ "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", @@ -72,6 +129,14 @@ "index": "pypi", "version": "==1.6.1" }, + "flask-bcrypt": { + "hashes": [ + "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a", + "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369" + ], + "index": "pypi", + "version": "==1.0.1" + }, "flask-cors": { "hashes": [ "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", diff --git a/src/api/routes.py b/src/api/routes.py index 9abdfdec9f..06db56bcac 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -7,8 +7,11 @@ from flask_cors import CORS from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required,verify_jwt_in_request from flask_jwt_extended.exceptions import NoAuthorizationError +from flask_bcrypt import Bcrypt api = Blueprint('api', __name__) +bcrypt = Bcrypt() + CORS(api) # @api.route('/hello', methods=['POST', 'GET']) # def handle_hello(): @@ -114,10 +117,11 @@ def get_one_account_to_one_user(user_id): def signup(): body = request.json # para manejo de errores poner exactamente el nombre del front igual en los campos entre parentesis (esperar a que se haga el front) - if not body or not body.get("email") or not body.get("password") or not body.get("lastname")or not body.get("firstname")or not body.get("birthday")or not body.get("country"): + if not body or not body.get("email") or not body.get("password") or not body.get("last_name")or not body.get("first_name")or not body.get("birthdate")or not body.get("country"): return jsonify({"msg": "missing fields"}), 400 + hashe_password = bcrypt.generate_password_hash(body["password"]) # encajar con los nombres del front estos (solo los que estan entre comillas) - new_user = User(email = body["email"],password= body["password"],last_name= body["lastname"],first_name= body["firstname"],birthday= body["birthday"],country= body["country"]) + new_user = User(email = body["email"],password=hashe_password, last_name= body["last_name"],first_name= body["first_name"],birthdate= body["birthdate"],country= body["country"]) db.session.add(new_user) db.session.commit() return jsonify({"msg": "user created"}), 201 \ No newline at end of file diff --git a/src/app.py b/src/app.py index ee9ad53fec..f3ba1549b4 100644 --- a/src/app.py +++ b/src/app.py @@ -11,6 +11,7 @@ from api.admin import setup_admin from api.commands import setup_commands from flask_jwt_extended import JWTManager +from flask_bcrypt import Bcrypt # from models import Person @@ -35,6 +36,7 @@ # Setup the Flask-JWT-Extended extension app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! jwt = JWTManager(app) +bcrypt = Bcrypt(app) # add the admin setup_admin(app) diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js index d2d14c6ff3..e50b6e9cf0 100644 --- a/src/front/js/component/login-form.js +++ b/src/front/js/component/login-form.js @@ -29,17 +29,22 @@ export const LoginForm = () => { return (
-
- +
+ setEmail(e.target.value)} value={email} /> - {!invalidAccount ?
Nunca compartiremos su correo electrónico con nadie más.
- :
Correo o Contraseña errada
}
-
- +
+ setPassword(e.target.value)} value={password} /> + {!invalidAccount ?
Nunca compartiremos su correo electrónico con nadie más.
+ :
Correo o Contraseña incorrectos
} +
+
+ +
+

¿Aun no estás registrado?

+
- ); }; \ No newline at end of file diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js index ecb4df2236..1a36926d38 100644 --- a/src/front/js/component/sidebar.js +++ b/src/front/js/component/sidebar.js @@ -24,11 +24,8 @@ export const Sidebar = () => { }; try { - const response = await fetch(`${process.env.BACKEND_URL}/api/user/1`, requestOptions); + const response = await fetch(`${process.env.BACKEND_URL}/api/user/2`, requestOptions); const result = await response.json(); - console.log(result); - - setUserName(result.result.first_name.toUpperCase()) setLastUserName(result.result.last_name.toUpperCase()) } catch (error) { diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index 600b9026e6..545c522286 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -15,8 +15,8 @@ const getState = ({ getStore, getActions, setStore }) => { } ], logged: false, - user:"", - auth:false + user: "", + auth: false }, actions: { // Use getActions to call a function within a fuction @@ -25,14 +25,14 @@ const getState = ({ getStore, getActions, setStore }) => { }, getMessage: async () => { - try{ + try { // fetching data from the backend const resp = await fetch(process.env.BACKEND_URL + "/api/hello") const data = await resp.json() setStore({ message: data.message }) // don't forget to return something, that is how the async resolves return data; - }catch(error){ + } catch (error) { console.log("Error loading message from backend", error) } }, @@ -71,7 +71,7 @@ const getState = ({ getStore, getActions, setStore }) => { const result = await response.json(); if (response.status === 200) { localStorage.setItem("token", result.access_token) - setStore({ logged: true }) + setStore({ logged: true }) getActions().verifyToken() getActions().getPrivate() } else if (response.status === 404 || response.status === 401) { diff --git a/src/front/styles/login.css b/src/front/styles/login.css index 05679cb7a8..3ad843d095 100644 --- a/src/front/styles/login.css +++ b/src/front/styles/login.css @@ -18,4 +18,30 @@ .invalidAccount{ color: red; font-weight: bold; +} +.buttons{ + flex-direction: column; + justify-content: center; + text-align: center; +} +.buttons p{ + margin-bottom: 10px; +} +.form-text{ + font-size: 10px; +} +.input-container{ + margin: 5px 0; + +} +.hr-login{ + margin: 15px 0 5px 0; +} +.create-user{ + font-size: 15px; + width: 60%; +} +.login-user{ + width: 90%; + font-size: 17px; } \ No newline at end of file diff --git a/src/instance/test.db b/src/instance/test.db index 7b410e4579000d8a43661d1e52f6fa5474d918fe..02f180081d84382332f271165a0bce7f7c514dac 100644 GIT binary patch delta 587 zcmZp8z}WDBae_1>|3n#QM*fWnOY{ZUc&{_?pXQ&!uf(^W&x`l^W~vhDIu`LB47JWlnDT#ish@`Qc@$<;JNlrCH(L z{+^K)nSL2n;g&f?p&@QnuFmFVW_q43232L=AfrIo$}Gy@K||~etJoLNttVSVQxxAscWI5i(^D-YGP1IwtG@h zUa@7Cc~DVBL9tOvXp%>+qh66)T5x75UONkzIT$4zF%NY0DdbcPq8l$VB??8z`vUR z2LD_Bjr`O3j{>8%ncq>ES(_2$T%_e2?IM(&LXOY}LI_@^-NpXQ&kSx}&ZfASRj0-(qZ2L2oTZ-F9b L_&4A1*H8ce{pJ#C From abc235d7ab32309db6d1f0a465a06e8dfe546ac4 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:26:25 +0000 Subject: [PATCH 20/45] resolviendo conflictos --- src/front/js/component/card.js | 30 +++++++++++++++++++++++++++--- src/front/js/component/sidebar.js | 13 +++++-------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js index dcbd1c4b2c..6dd3d4ff24 100644 --- a/src/front/js/component/card.js +++ b/src/front/js/component/card.js @@ -1,6 +1,30 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; export const Card = () => { + const [userNameAccounts, setUserNameAccounts] = useState("") + + + async function getAccountsUser() { + const myHeaders = new Headers(); + myHeaders.append("Cookie", ".Tunnels.Relay.WebForwarding.Cookies=CfDJ8Cs4yarcs6pKkdu0hlKHsZtLJEdvHSwRXZt17-888_aVJrmBrvu6RtYBoKMpKm9sEC-AxXi3sRWKgbFwf-p4hj2-84XL-87bz_Y0G7zB8iX-0y0kE4onkjgxSurJ2xjcvjN5j0SznVQpWIrQcTHHpW6OV8NIiNT91dMN_DBpqn7Ku0inf13JGgK7eJyftdxiu-_vwQHTzVbsiQN30MKU5UG7jMXXLPbrbXHRtaabX2EnUEHxglQZieSfFmtvDEvoGq26C0ixTRdmpVddBdux-ENmx93bhGaMFMQoE6ieOHEM83IjYztMG5XWr9WXCNyA1uGfaxE2rWP9qtlcvPoOYfgN6ci_aqRSxMumE5s_yvdu62T4sRgFVhMGDTrKF4hsNtkxzk7-6yHbnQMbXGg1BrVNUA7nZp5aj6i7lp67WQpPN3HqPkM0hUFDodFluRq3WaibUehanTF0-ewI7h5X1LsMbv-lO1qi7d2QsYT2jxyfxqO17Iz9os0znyuhBTZk7naL3yyXiwLpwzk6yyLHiEotFHha8NPLbJ9qdLu_rKxVO8xQyG8xYErsUqSl-x9PArSo6oudhd59gqyVxuPsQSVcKz-trvEPExQEVUXHK8gYdpnK3HwpenhoR-GqCxXjWtG1qXiRC78se1u9qYaFloMPMnqfZjkeHQAN5y_f2-2PcaX76KvzDWCwqsuWuYk5mq4yJxc6vdF2w0A2oIpPJn-s9aIA-0G9zo2FneXGU6WH10y8G430F-E9YqNjfepj32J53HEfcQS5mcl_WV8V98etWQS7FserlTT__EW-eM1dI3UvVAHV5ptaSNhbqaI8OXBjfm0onnpmrN5QqBn3W9tQRO6q1A_H7UhRGVUb5IFb-TKC033oG4rYb__EwxNQ1rx3uaXqahbtxSDNnASci2j_jHRQOIp68krpmiZT_BTb80OYh8znnE-L-JdM25WYEoIi5WUKtjyl5P0Wi_-ZN8IkCqcCuruodh4wn9L5TjeyLeXYTcBN8ltxypEXbt6F5g"); + + const requestOptions = { + method: "GET", + headers: myHeaders, + redirect: "follow" + }; + + try { + const response = await fetch(`${process.env.BACKEND_URL}/api/user/1/accounts`, requestOptions); + const result = await response.json(); + console.log(result) + } catch (error) { + console.error(error); + }; + useEffect(() => { + getAccountsUser() + }, []) + } return (
@@ -8,8 +32,8 @@ export const Card = () => {

Saldo

- - + +

Moneda

diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js index ecb4df2236..f51654f5f4 100644 --- a/src/front/js/component/sidebar.js +++ b/src/front/js/component/sidebar.js @@ -1,4 +1,4 @@ -import React, { useEffect, useContext,useState } from "react"; +import React, { useEffect, useContext, useState } from "react"; import { Context } from "../store/appContext"; import { Link } from "react-router-dom"; import "/src/front/styles/sidebar.css"; @@ -8,10 +8,10 @@ export const Sidebar = () => { const [userName, setUserName] = useState("") const [userLastName, setLastUserName] = useState("") const { store, actions } = useContext(Context) - - const handleClick = () => { - actions.logout() - } + + const handleClick = () => { + actions.logout() + } async function getUser() { const myHeaders = new Headers(); @@ -26,9 +26,6 @@ export const Sidebar = () => { try { const response = await fetch(`${process.env.BACKEND_URL}/api/user/1`, requestOptions); const result = await response.json(); - console.log(result); - - setUserName(result.result.first_name.toUpperCase()) setLastUserName(result.result.last_name.toUpperCase()) } catch (error) { From 61bc2d4ab7a74f6c0b25854773a000ad36788668 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:16:03 +0000 Subject: [PATCH 21/45] =?UTF-8?q?se=20termin=C3=B3=20de=20configurar=20el?= =?UTF-8?q?=20login,=20con=20validadores=20de=20token=20y=20se=20agregaron?= =?UTF-8?q?=20estilos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/routes.py | 11 +++ src/front/js/component/login-form.js | 14 +-- src/front/js/component/sidebar.js | 1 - src/front/js/pages/principalPage.js | 21 ++++- src/front/js/store/appContext.js | 2 +- src/front/js/store/flux.js | 106 +++++++++++------------ src/front/styles/container.css | 1 - src/front/styles/index.css | 122 +++++++++++++++++++++++++++ src/instance/test.db | Bin 28672 -> 28672 bytes 9 files changed, 209 insertions(+), 69 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index 06db56bcac..b9646cedca 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -60,7 +60,17 @@ def protected(): # Access the identity of the current user with get_jwt_identity current_user = get_jwt_identity() return jsonify(logged_in_as=current_user), 200 + +@api.route("/verify-token", methods=["GET"]) +def verify_token(): + try: + verify_jwt_in_request() # Verifica la validez del token + identity = get_jwt_identity() # Obtiene el usuario del token + return jsonify({"valid": True, "user": identity}), 200 + except NoAuthorizationError: + return jsonify({"valid": False, "message": "Token inválido o no proporcionado"}), 401 #este endpoint crea una cuenta con el id del usuario + @api.route('//new-account', methods=['POST']) def post_account(user_id): try: @@ -110,6 +120,7 @@ def get_one_account_to_one_user(user_id): except Exception as e: return jsonify({"msg":"Error", "error": str(e)}), 500 + # endpoints # registro usuario diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js index e50b6e9cf0..ec4fadfad1 100644 --- a/src/front/js/component/login-form.js +++ b/src/front/js/component/login-form.js @@ -8,24 +8,26 @@ export const LoginForm = () => { const [password, setPassword] = useState("") const { store, actions } = useContext(Context) let navigate = useNavigate(); - + async function handleSubmit(e) { e.preventDefault() await actions.login(email, password) - if (!store.logged) { + if (!store.auth) { setInvalidAccount(true) } else { setInvalidAccount(false) } } useEffect(() => { - if (store.logged) { + if (store.auth) { navigate("/cuentas"); + } else { + navigate("/"); } if (invalidAccount) { setInvalidAccount(false) } - }, [store.logged]) + }, [store.auth]) return (
@@ -41,9 +43,9 @@ export const LoginForm = () => {
-
+

¿Aun no estás registrado?

- +
); diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js index 1a36926d38..e630fd36ea 100644 --- a/src/front/js/component/sidebar.js +++ b/src/front/js/component/sidebar.js @@ -3,7 +3,6 @@ import { Context } from "../store/appContext"; import { Link } from "react-router-dom"; import "/src/front/styles/sidebar.css"; - export const Sidebar = () => { const [userName, setUserName] = useState("") const [userLastName, setLastUserName] = useState("") diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index ca100ff101..003cf5107b 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -1,9 +1,24 @@ -import React from "react"; +import React, { useContext, useEffect } from "react"; import { Sidebar } from "../component/sidebar"; import { Card } from "../component/card"; import "../../styles/container.css"; +import { Context } from "../store/appContext"; +import { Link, useNavigate } from "react-router-dom"; + export const PrincipalPage = () => { + const { store, actions } = useContext(Context) + let navigate = useNavigate(); + + useEffect(() => { + actions.verifyToken(); + if (!store.auth) { + navigate("/"); + } + }, []); + + if (!store.auth) return null; // Evita que se renderice la UI antes de la redirección + return (
@@ -15,9 +30,9 @@ export const PrincipalPage = () => { -
+
- ) + ); } \ No newline at end of file diff --git a/src/front/js/store/appContext.js b/src/front/js/store/appContext.js index 1ea6530b01..6b2c118d8d 100755 --- a/src/front/js/store/appContext.js +++ b/src/front/js/store/appContext.js @@ -29,7 +29,7 @@ const injectContext = PassedComponent => { * store, instead use actions, like this: **/ state.actions.getMessage(); // <---- calling this function from the flux.js actions - // state.actions.verifyToken(); + state.actions.verifyToken(); }, []); // The initial value for the context is not null anymore, but the current state of this component, diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index 545c522286..0f337d4bd3 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -14,110 +14,102 @@ const getState = ({ getStore, getActions, setStore }) => { initial: "white" } ], - logged: false, user: "", auth: false }, actions: { - // Use getActions to call a function within a fuction exampleFunction: () => { getActions().changeColor(0, "green"); }, getMessage: async () => { try { - // fetching data from the backend - const resp = await fetch(process.env.BACKEND_URL + "/api/hello") - const data = await resp.json() - setStore({ message: data.message }) - // don't forget to return something, that is how the async resolves + const resp = await fetch(process.env.BACKEND_URL + "/api/hello"); + const data = await resp.json(); + setStore({ message: data.message }); return data; } catch (error) { - console.log("Error loading message from backend", error) + console.log("Error loading message from backend", error); } }, + changeColor: (index, color) => { - //get the store const store = getStore(); - - //we have to loop the entire demo array to look for the respective index - //and change its color const demo = store.demo.map((elm, i) => { if (i === index) elm.background = color; return elm; }); - - //reset the global store - setStore({ demo: demo }); + setStore({ demo }); }, - login: async (email, password) => { - const myHeaders = new Headers(); - myHeaders.append("Content-Type", "application/json"); - - const raw = JSON.stringify({ - "email": email, - "password": password - }); - - const requestOptions = { - method: "POST", - headers: myHeaders, - body: raw, - redirect: "follow" - }; + login: async (email, password) => { try { - const response = await fetch(`${process.env.BACKEND_URL}/api/login`, requestOptions); + const response = await fetch(`${process.env.BACKEND_URL}/api/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); const result = await response.json(); + if (response.status === 200) { - localStorage.setItem("token", result.access_token) - setStore({ logged: true }) - getActions().verifyToken() - getActions().getPrivate() - } else if (response.status === 404 || response.status === 401) { - setStore({ logged: false }) + localStorage.setItem("token", result.access_token); + setStore({ auth: true }); + getActions().verifyToken(); + getActions().getPrivate(); + } else { + setStore({ auth: false }); } } catch (error) { console.error(error); - return false; - }; + setStore({ auth: false }); + } }, + getPrivate: async () => { - let token = localStorage.getItem("token") + let token = localStorage.getItem("token"); + if (!token) return; + try { - const response = await fetch(`${process.env.BACKEND_URL}/api/private`, { + const response = await fetch(`${process.env.BACKEND_URL}/api/protected`, { method: "GET", - headers: { - "Authorization": `Bearer ${token}` - }, + headers: { "Authorization": `Bearer ${token}` }, }); const result = await response.json(); - setStore({ user: result.logged_in_as }) + if (response.status === 200) { + setStore({ user: result.logged_in_as }); + } } catch (error) { console.error(error); - }; + } }, + verifyToken: async () => { - let token = localStorage.getItem("token") + let token = localStorage.getItem("token"); + if (!token) { + setStore({ auth: false }); + return; + } + try { const response = await fetch(`${process.env.BACKEND_URL}/api/verify-token`, { method: "GET", - headers: { - "Authorization": `Bearer ${token}` - }, + headers: { "Authorization": `Bearer ${token}` }, }); - const result = await response.json(); - setStore({ auth: result.valid }) + const isAuthenticated = response.status === 200; + + if (getStore().auth !== isAuthenticated) { + setStore({ auth: isAuthenticated }); + } } catch (error) { console.error(error); - }; + setStore({ auth: false }); + } }, + logout: () => { - //borrar el token del localStorage - localStorage.removeItem("token") - setStore({ logged: false }) - getActions().verifyToken() + localStorage.removeItem("token"); + setStore({ auth: false, user: "" }); }, } }; diff --git a/src/front/styles/container.css b/src/front/styles/container.css index 937cd1b521..e00c03524b 100644 --- a/src/front/styles/container.css +++ b/src/front/styles/container.css @@ -10,6 +10,5 @@ } .row{ padding: 2rem; - background-color:antiquewhite; height: 100vh; } \ No newline at end of file diff --git a/src/front/styles/index.css b/src/front/styles/index.css index 1ac0e879a5..d7257d2ebc 100755 --- a/src/front/styles/index.css +++ b/src/front/styles/index.css @@ -1,3 +1,125 @@ /* General Styles used on every website (Don't Repeat Yourself) */ +/* IMPORTAR FUENTES */ +@import url('https://fonts.googleapis.com/css2?family=Anton&family=Roboto:wght@300;400;700&display=swap'); + +/* VARIABLES DE COLOR */ +:root { + --primary: #010D87; + --background-light: #f5f5f5; + --background-dark: #424242; + --text-light: #010D87; + --text-dark: #f5f5f5; + --gray-light: #e0e0e0; + --gray-dark: #757575; +} + +/* MODO OSCURO */ +[data-theme="dark"] { + --background-light: #212121; + --background-dark: #121212; + --text-light: #f5f5f5; + --text-dark: #010D87; + --gray-light: #424242; + --gray-dark: #bdbdbd; +} + +/* APLICACIÓN GLOBAL */ +body { + font-family: 'Roboto', sans-serif; + background-color: var(--background-light); + color: var(--text-light); + transition: background 0.3s, color 0.3s; +} + +/* ESTILOS PARA LOS TÍTULOS */ +h1, h2, h3 { + font-family: 'Anton', sans-serif; + color: var(--primary); +} + +/* BOTÓN DE CAMBIO DE TEMA */ +.toggle-theme { + position: fixed; + top: 20px; + right: 20px; + background: var(--primary); + color: var(--text-dark); + border: none; + padding: 10px 15px; + cursor: pointer; + border-radius: 10px; + font-weight: bold; + transition: background 0.3s; +} + +.toggle-theme:hover { + background: var(--gray-dark); +} + +/* ESTILOS PARA BOTONES */ +.btn { + padding: 10px 20px; + border-radius: 10px; + font-family: "Anton", sans-serif; + font-size: 16px; + border: none; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; +} + +.btn-primary { + background-color: var(--primary); + color: white; + box-shadow: 0px 4px 10px rgba(1, 13, 135, 0.3); +} + +.btn-primary:hover { + background-color: #0026ff; + box-shadow: 0px 6px 15px rgba(1, 13, 135, 0.5); +} + +.btn-secondary { + background-color: var(--gray-light); + color: var(--primary); +} + +.btn-secondary:hover { + background-color: var(--gray-dark); + color: white; +} + +.btn-theme { + background-color: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-theme:hover { + background-color: var(--primary); + color: white; +} + +/* ESTILOS PARA EL MODO OSCURO */ +[data-theme="dark"] .btn-primary { + background-color: white; + color: var(--primary); + box-shadow: 0px 4px 10px rgba(255, 255, 255, 0.3); +} + +[data-theme="dark"] .btn-primary:hover { + background-color: var(--gray-light); + box-shadow: 0px 6px 15px rgba(255, 255, 255, 0.5); +} + +[data-theme="dark"] .btn-theme { + color: white; + border-color: white; +} + +[data-theme="dark"] .btn-theme:hover { + background-color: white; + color: var(--primary); +} diff --git a/src/instance/test.db b/src/instance/test.db index 02f180081d84382332f271165a0bce7f7c514dac..1ac83d20236212ebf543372b215b8e1a24fd2f0c 100644 GIT binary patch delta 228 zcmZp8z}WDBae_3Xz(g5mMuCk9OZ0`E+S*Vw9WqEj|dsuc(a&}Tgu3KSZL261-KFCA^Lwz7Jw6rt;@e0{E z8MGT&L}eAFL0sd_&uwLygxL8T8TeQ8-{61Czmb1Be f0uTldmjDl|57-aw53vuI55=<)5Q7i1mrrRRSdSOD From 70fc98c7e7199e401ac9ec0f4a23a615f0227035 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Tue, 4 Mar 2025 23:56:18 +0000 Subject: [PATCH 22/45] se crearon funciones en el flux para capturar al usuario regitrado y poder consumir sus datos dentro de la app --- src/front/js/component/sidebar.js | 36 ++++------------------ src/front/js/pages/principalPage.js | 3 +- src/front/js/store/flux.js | 48 +++++++++++++++++++++++------ 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/front/js/component/sidebar.js b/src/front/js/component/sidebar.js index e630fd36ea..432a754e31 100644 --- a/src/front/js/component/sidebar.js +++ b/src/front/js/component/sidebar.js @@ -1,39 +1,15 @@ -import React, { useEffect, useContext,useState } from "react"; +import React, { useEffect, useContext, useState } from "react"; import { Context } from "../store/appContext"; import { Link } from "react-router-dom"; import "/src/front/styles/sidebar.css"; export const Sidebar = () => { - const [userName, setUserName] = useState("") - const [userLastName, setLastUserName] = useState("") const { store, actions } = useContext(Context) - - const handleClick = () => { - actions.logout() - } - async function getUser() { - const myHeaders = new Headers(); - myHeaders.append("Cookie", ".Tunnels.Relay.WebForwarding.Cookies=CfDJ8Cs4yarcs6pKkdu0hlKHsZsY5nM0Bh-G8KdaZucRe2wc-P0OBfWNR4e_Ry-wYVh6y7msxvQ0bt_A84DgHodrU_OCCrKzu1cgD7lBCg0Y9XtWdLuWjQ5bDQEa6nI-aPNG721L5Swl3R7MTaWyc63l2Gfva0VAPJd5aTjfuIPHuEUwqsm5SBixx1f0LbKQSz3YRcuUinDNLwoMSypZCqgCRKw0vXaf_WPMxJXnwem-NNI6X2zDt3Ts0S7w44nPpfiSZgnkudY_gI48iqzvAxpwzKCWfCgP__g__IUpKfqJ96iiVcw05AtVf6MOhmmTS4q5bIt__wa4FeBk_GBHEspoCt-Rf_W2PmhdrGi7dbasxNmRqTjrhWf-1U6g1WoxuvUZrqwJ_D0u-NS5wFmDVQRY0JZjgUv-HnMddgQ32FlS6l8w9Q5l9KmMt7QliLxjvXlWcPbmiIhIrcDuPlr9NKFakMg9CFX-kuVToRFwszOO7kPJn5QbAeeYdxsx5OSAL2lTO2haHjRoHaAaLdfGBIDmOdKeiymLmcrw7ro0RER4FBmm9G-kNPIYGhYgI4NxpK9stDaRD4h49usa6GGyPzBN-iyXVh-Cz6w-QpoaauccXovZnnPSZcSLW-VO4tEiLkAiqgFoLq2tvwTam5z5ZCqfSwmxvYa87f10Fhd-kX-4thA_nLPhaXA4fC7cY0V2WvemkDP4MiykRK9x4RhI4BzIg3bxy1fX9bBQ7gggODLTokhOHSfPJ_JG9u-3YjgaeLdtOBby5h5Jk8AvpXBDG4bZNQAvhdvCfeOVdIW-SVLxZrypB8wh76WzWFDGsiZRXCnXtilF0VkPULs1VLOChY-cSAlrzpgFCA2OAwmY5QczvAn7oAMcf8_Rsc82Wg1NrsgmFqIoagVgUIaYsTaKMLvwnTKf06Q4jz01sGRqbDy_9c-QQ1v_OQjT2xYTbYjPsituCX0UUSeaYPEVyDEsOo1KAzM3mqPSSoJfLfYHPuf-26Wp"); - - const requestOptions = { - method: "GET", - headers: myHeaders, - redirect: "follow" - }; - - try { - const response = await fetch(`${process.env.BACKEND_URL}/api/user/2`, requestOptions); - const result = await response.json(); - setUserName(result.result.first_name.toUpperCase()) - setLastUserName(result.result.last_name.toUpperCase()) - } catch (error) { - console.error(error); - }; + const handleClick = () => { + actions.logout() } - useEffect(() => { - getUser() - }, []) + return (
@@ -48,8 +24,8 @@ export const Sidebar = () => { slogan

- -

{userName} {userLastName}

+ +

{store.user.first_name} {store.user.last_name}

    diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 003cf5107b..444d153c94 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -12,12 +12,13 @@ export const PrincipalPage = () => { useEffect(() => { actions.verifyToken(); + actions.initializeStore() if (!store.auth) { navigate("/"); } }, []); - if (!store.auth) return null; // Evita que se renderice la UI antes de la redirección + if (!store.auth) return null; return (
    diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index 0f337d4bd3..dc7c86f80d 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -14,8 +14,8 @@ const getState = ({ getStore, getActions, setStore }) => { initial: "white" } ], - user: "", - auth: false + user: JSON.parse(localStorage.getItem("userLogged")) || "", // Cargar el usuario desde localStorage email: "", + auth: !!localStorage.getItem("token"), // Verifica si hay un token para mantener la sesión activa }, actions: { exampleFunction: () => { @@ -54,8 +54,9 @@ const getState = ({ getStore, getActions, setStore }) => { if (response.status === 200) { localStorage.setItem("token", result.access_token); setStore({ auth: true }); - getActions().verifyToken(); - getActions().getPrivate(); + await getActions().verifyToken(); + await getActions().getPrivate(); + await getActions().getUserLogged(); } else { setStore({ auth: false }); } @@ -68,17 +69,13 @@ const getState = ({ getStore, getActions, setStore }) => { getPrivate: async () => { let token = localStorage.getItem("token"); if (!token) return; - try { const response = await fetch(`${process.env.BACKEND_URL}/api/protected`, { method: "GET", headers: { "Authorization": `Bearer ${token}` }, }); const result = await response.json(); - - if (response.status === 200) { - setStore({ user: result.logged_in_as }); - } + setStore({ email: result.logged_in_as }) } catch (error) { console.error(error); } @@ -109,8 +106,41 @@ const getState = ({ getStore, getActions, setStore }) => { logout: () => { localStorage.removeItem("token"); + localStorage.removeItem("userLogged"); setStore({ auth: false, user: "" }); }, + getUserLogged: async () => { + + try { + const userLogged = JSON.parse(localStorage.getItem("userLogged")); + const store = getStore() + if (userLogged === null && store.email !== undefined) { + const response = await fetch(`${process.env.BACKEND_URL}/api/users`); + const result = await response.json(); + const userLogged = await result.results.find(item => item.email === store.email); + localStorage.setItem("userLogged", JSON.stringify(userLogged)); + setStore({ user: userLogged }); + } else { + setStore({ user: userLogged }) + } + } catch (error) { + console.error(error); + } + }, + initializeStore: () => { + const userLogged = JSON.parse(localStorage.getItem("userLogged")); + const token = localStorage.getItem("token"); + + if (userLogged) { + setStore({ user: userLogged }); + } + + if (token) { + setStore({ auth: true }); + getActions().verifyToken(); + getActions().getPrivate(); + } + }, } }; }; From 948291860229cc0008dd80d6f8f2ee06e63dee22 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Wed, 5 Mar 2025 09:47:53 +0000 Subject: [PATCH 23/45] correcion de errores --- src/front/js/component/card.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js index 6dd3d4ff24..4526d556a0 100644 --- a/src/front/js/component/card.js +++ b/src/front/js/component/card.js @@ -2,6 +2,9 @@ import React, { useEffect, useState } from "react"; export const Card = () => { const [userNameAccounts, setUserNameAccounts] = useState("") + const [userCoinAccounts, setUserCoinAccounts] = useState("") + const [userBalanceAccounts, setUserBalanceAccounts] = useState("") + const [userTypeAccounts, setUserTypeAccounts] = useState("") async function getAccountsUser() { @@ -17,14 +20,16 @@ export const Card = () => { try { const response = await fetch(`${process.env.BACKEND_URL}/api/user/1/accounts`, requestOptions); const result = await response.json(); - console.log(result) + console.log(result.result) + } catch (error) { console.error(error); }; - useEffect(() => { - getAccountsUser() - }, []) + } + useEffect(() => { + getAccountsUser() + }, []) return (
    From 39ac33d097575790673499295f4088953e691538 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:43:57 +0000 Subject: [PATCH 24/45] =?UTF-8?q?creaci=C3=B3n=20de=20la=20rama=20para=20s?= =?UTF-8?q?olucionar=20bugs=20de=20base=20de=20datos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/instance/test.db | Bin 28672 -> 28672 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/instance/test.db b/src/instance/test.db index 1ac83d20236212ebf543372b215b8e1a24fd2f0c..3c57fe3f90d5f85acb5caa89208e3a8589a4fbb8 100644 GIT binary patch delta 620 zcmZp8z}WDBae_3X$V3@uMv;vPOZ26#U*`4y!L5|eWB4Gjz((sL6tbM%t)b5)FzR1A$&TvCGy%ER@u%Uz0# zGSW(XbNqcXvP$!uGfIkzDhf>!&GP-CTwMZ9k^_x0j6MCcJuK5I69Yh|1sWLY8v>D~ zrKxLiLE_y4EhNB>VXzw*yj;e;qLC~m|Yd+@0ac4Q(#w?r?{yUZ=5$U;9VIX%e5&!|wp)Hp9VBhV?tGRerv*%^m@IUJk}x(NT}V}@p8 zxRYL3NmRO-g}YmTy+?tUQJF!hbE$KfNmyWDag|e5R!Tr>R*IjouWM*-+@1jKW8(KgD8K57_%W`0Y;p1i!$rOgb_i(g%Cvw TAWjixU8q`wQjX1y{`Cp~8uh+O delta 77 zcmZp8z}WDBae_3Xz(g5mMuCk9OZ0` Date: Wed, 5 Mar 2025 11:45:31 +0000 Subject: [PATCH 25/45] =?UTF-8?q?se=20cambi=C3=B3=20la=20logitud=20permiti?= =?UTF-8?q?da=20para=20las=20contrase=C3=B1as?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/models.py b/src/api/models.py index 0cdfc1ef1b..5e633206ec 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -9,7 +9,7 @@ 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(String(80),nullable=False) + password: Mapped[str] = mapped_column(String(256),nullable=False) first_name: Mapped[str] = mapped_column(String(120),nullable=False) last_name: Mapped[str] = mapped_column(String(120),nullable=False) birthdate: Mapped[str] = mapped_column(String(80),nullable=False) From 180882839cef2e8e1a6d453564449b11006e18fc Mon Sep 17 00:00:00 2001 From: LauraPostigo <166739141+LauraPostigo@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:53:45 +0000 Subject: [PATCH 26/45] forth --- .../versions/30d819e19461_.py | 10 +++++----- src/api/models.py | 2 +- src/api/routes.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename "migrations/versions/a9589fe71e35_inicializaci\303\263n_de_la_base_de_datos.py" => migrations/versions/30d819e19461_.py (91%) diff --git "a/migrations/versions/a9589fe71e35_inicializaci\303\263n_de_la_base_de_datos.py" b/migrations/versions/30d819e19461_.py similarity index 91% rename from "migrations/versions/a9589fe71e35_inicializaci\303\263n_de_la_base_de_datos.py" rename to migrations/versions/30d819e19461_.py index b0f75ff6da..08bf9c6997 100644 --- "a/migrations/versions/a9589fe71e35_inicializaci\303\263n_de_la_base_de_datos.py" +++ b/migrations/versions/30d819e19461_.py @@ -1,8 +1,8 @@ -"""Inicialización de la base de datos +"""empty message -Revision ID: a9589fe71e35 +Revision ID: 30d819e19461 Revises: -Create Date: 2025-03-01 10:25:26.199988 +Create Date: 2025-03-05 10:55:41.951838 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = 'a9589fe71e35' +revision = '30d819e19461' down_revision = None branch_labels = None depends_on = None @@ -21,7 +21,7 @@ def upgrade(): 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(length=80), nullable=False), + sa.Column('password', sa.String(length=256), nullable=False), sa.Column('first_name', sa.String(length=120), nullable=False), sa.Column('last_name', sa.String(length=120), nullable=False), sa.Column('birthdate', sa.String(length=80), nullable=False), diff --git a/src/api/models.py b/src/api/models.py index 0cdfc1ef1b..5e633206ec 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -9,7 +9,7 @@ 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(String(80),nullable=False) + password: Mapped[str] = mapped_column(String(256),nullable=False) first_name: Mapped[str] = mapped_column(String(120),nullable=False) last_name: Mapped[str] = mapped_column(String(120),nullable=False) birthdate: Mapped[str] = mapped_column(String(80),nullable=False) diff --git a/src/api/routes.py b/src/api/routes.py index b9646cedca..511630cd7f 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -47,7 +47,7 @@ def login(): password = request.json.get("password", None) try: user = db.session.execute(db.select(User).filter_by(email=email)).scalar_one() - if password != user.password: + if bcrypt.checkpw(password.encode("utf-8"), user.password): return jsonify({"msg": "Bad email or password"}), 401 access_token = create_access_token(identity=email) return jsonify(access_token=access_token) @@ -130,7 +130,7 @@ def signup(): # para manejo de errores poner exactamente el nombre del front igual en los campos entre parentesis (esperar a que se haga el front) if not body or not body.get("email") or not body.get("password") or not body.get("last_name")or not body.get("first_name")or not body.get("birthdate")or not body.get("country"): return jsonify({"msg": "missing fields"}), 400 - hashe_password = bcrypt.generate_password_hash(body["password"]) + hashe_password = bcrypt.generate_password_hash(body["password"]).decode("utf-8") # encajar con los nombres del front estos (solo los que estan entre comillas) new_user = User(email = body["email"],password=hashe_password, last_name= body["last_name"],first_name= body["first_name"],birthdate= body["birthdate"],country= body["country"]) db.session.add(new_user) From ed7cb1d933b49de088089632927632c6b239ff27 Mon Sep 17 00:00:00 2001 From: Luis Malizia Date: Wed, 5 Mar 2025 12:34:24 +0000 Subject: [PATCH 27/45] =?UTF-8?q?se=20agrego=20la=20funcion=20para=20desen?= =?UTF-8?q?criptar=20la=20contrase=C3=B1a=20en=20el=20endpoint=20de=20logi?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/routes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index b9646cedca..a5d34df216 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -5,7 +5,7 @@ from api.models import db, User, Accounts from api.utils import generate_sitemap, APIException from flask_cors import CORS -from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required,verify_jwt_in_request +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required,verify_jwt_in_request, decode_token from flask_jwt_extended.exceptions import NoAuthorizationError from flask_bcrypt import Bcrypt @@ -47,7 +47,7 @@ def login(): password = request.json.get("password", None) try: user = db.session.execute(db.select(User).filter_by(email=email)).scalar_one() - if password != user.password: + if not bcrypt.check_password_hash(user.password, password): return jsonify({"msg": "Bad email or password"}), 401 access_token = create_access_token(identity=email) return jsonify(access_token=access_token) @@ -130,7 +130,7 @@ def signup(): # para manejo de errores poner exactamente el nombre del front igual en los campos entre parentesis (esperar a que se haga el front) if not body or not body.get("email") or not body.get("password") or not body.get("last_name")or not body.get("first_name")or not body.get("birthdate")or not body.get("country"): return jsonify({"msg": "missing fields"}), 400 - hashe_password = bcrypt.generate_password_hash(body["password"]) + hashe_password = bcrypt.generate_password_hash(body["password"]).decode("utf-8") # encajar con los nombres del front estos (solo los que estan entre comillas) new_user = User(email = body["email"],password=hashe_password, last_name= body["last_name"],first_name= body["first_name"],birthdate= body["birthdate"],country= body["country"]) db.session.add(new_user) From e4096d99ac50c119771b9b081dfb5979c4423a9f Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Wed, 5 Mar 2025 12:39:43 +0000 Subject: [PATCH 28/45] fix bugs --- src/front/js/component/card.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js index 42752beef4..97163f3176 100644 --- a/src/front/js/component/card.js +++ b/src/front/js/component/card.js @@ -1,12 +1,13 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useContext} from "react"; import { Context } from "../store/appContext"; export const Card = () => { const { store, actions } = useContext(Context) - const [userNameAccounts, setUserNameAccounts] = useState("") - const [userCoinAccounts, setUserCoinAccounts] = useState("") - const [userBalanceAccounts, setUserBalanceAccounts] = useState("") - const [userTypeAccounts, setUserTypeAccounts] = useState("") + const [userAccounst,setUserAccounts]=useState([]) + // const [userNameAccounts, setUserNameAccounts] = useState("") + // const [userCoinAccounts, setUserCoinAccounts] = useState("") + // const [userBalanceAccounts, setUserBalanceAccounts] = useState("") + // const [userTypeAccounts, setUserTypeAccounts] = useState("") async function getAccountsUser() { @@ -22,7 +23,9 @@ export const Card = () => { try { const response = await fetch(`${process.env.BACKEND_URL}/api/user/1/accounts`, requestOptions); const result = await response.json(); - console.log(result.result) + setUserAccounts(result.result) + console.log(userAccounst); + } catch (error) { console.error(error); From 366b5adccf39f4b3e54502f79986a480cc59dc37 Mon Sep 17 00:00:00 2001 From: Luis Malizia Date: Wed, 5 Mar 2025 13:49:07 +0000 Subject: [PATCH 29/45] se creo el fetch para mostrar el balance de una cuenta, faltan los validadores --- src/api/routes.py | 2 +- src/front/js/component/balanceGeneral.js | 54 ++++++++++++++++++++++++ src/front/js/pages/principalPage.js | 2 + 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/front/js/component/balanceGeneral.js diff --git a/src/api/routes.py b/src/api/routes.py index a5d34df216..bb4cf0c70b 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -69,8 +69,8 @@ def verify_token(): return jsonify({"valid": True, "user": identity}), 200 except NoAuthorizationError: return jsonify({"valid": False, "message": "Token inválido o no proporcionado"}), 401 - #este endpoint crea una cuenta con el id del usuario + #este endpoint crea una cuenta con el id del usuario @api.route('//new-account', methods=['POST']) def post_account(user_id): try: diff --git a/src/front/js/component/balanceGeneral.js b/src/front/js/component/balanceGeneral.js new file mode 100644 index 0000000000..3f145aacdb --- /dev/null +++ b/src/front/js/component/balanceGeneral.js @@ -0,0 +1,54 @@ +import React, { useState, useEffect } from "react"; +import { useContext } from "react"; +import { Context } from "../store/appContext"; + +export const GeneralBalance = () => { + const [balance, setBalance] = useState(0); + + const {store,actions} = useContext(Context) + + const generalBalance = async () => { + const myHeaders = new Headers(); + myHeaders.append( + "Cookie", + ".Tunnels.Relay.WebForwarding.Cookies=CfDJ8Cs4yar..." + ); + + const requestOptions = { + method: "GET", + headers: myHeaders, + redirect: "follow", + }; + + try { + const response = await fetch + (`${process.env.BACKEND_URL}/api/user/${store.user.id}/accounts`, + requestOptions + ); + const data = await response.json(); +// console.log(data[0].balance); +setBalance(data.result[0].balance); + + + + // Ajustar según la estructura real de la API + // if (data && Array.isArray(data.valores)) { + // const totalBalance = data.valores.reduce((acc, num) => acc + num, 0); + // setBalance(totalBalance); + // } else { + // console.error("Estructura de datos inesperada:", data); + // setError("Formato de datos incorrecto"); + // } + } catch (error) { + console.error("Error al obtener el balance:", error); + } + }; + + useEffect(() => { + generalBalance() + }, []); + + return ( +

    {balance} euros

    + ); +}; diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 444d153c94..5ed035173f 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -1,6 +1,7 @@ import React, { useContext, useEffect } from "react"; import { Sidebar } from "../component/sidebar"; import { Card } from "../component/card"; +import { GeneralBalance } from "../component/balanceGeneral"; import "../../styles/container.css"; import { Context } from "../store/appContext"; import { Link, useNavigate } from "react-router-dom"; @@ -26,6 +27,7 @@ export const PrincipalPage = () => {

    Balance general

    +
    From e2c76b329ede52c9f59e25d1a725daccf47d98b3 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Wed, 5 Mar 2025 23:27:48 +0000 Subject: [PATCH 30/45] se crea el map y se consume info de la bd y se muestra en la card --- src/front/js/component/card.js | 46 ++++------------------------ src/front/js/pages/principalPage.js | 39 ++++++++++++++++++----- src/instance/test.db | Bin 28672 -> 28672 bytes 3 files changed, 38 insertions(+), 47 deletions(-) diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js index 97163f3176..cc26a2c4ef 100644 --- a/src/front/js/component/card.js +++ b/src/front/js/component/card.js @@ -1,57 +1,23 @@ -import React, { useEffect, useState, useContext} from "react"; -import { Context } from "../store/appContext"; +import React from "react"; -export const Card = () => { - const { store, actions } = useContext(Context) - const [userAccounst,setUserAccounts]=useState([]) - // const [userNameAccounts, setUserNameAccounts] = useState("") - // const [userCoinAccounts, setUserCoinAccounts] = useState("") - // const [userBalanceAccounts, setUserBalanceAccounts] = useState("") - // const [userTypeAccounts, setUserTypeAccounts] = useState("") - - - async function getAccountsUser() { - const myHeaders = new Headers(); - myHeaders.append("Cookie", ".Tunnels.Relay.WebForwarding.Cookies=CfDJ8Cs4yarcs6pKkdu0hlKHsZtLJEdvHSwRXZt17-888_aVJrmBrvu6RtYBoKMpKm9sEC-AxXi3sRWKgbFwf-p4hj2-84XL-87bz_Y0G7zB8iX-0y0kE4onkjgxSurJ2xjcvjN5j0SznVQpWIrQcTHHpW6OV8NIiNT91dMN_DBpqn7Ku0inf13JGgK7eJyftdxiu-_vwQHTzVbsiQN30MKU5UG7jMXXLPbrbXHRtaabX2EnUEHxglQZieSfFmtvDEvoGq26C0ixTRdmpVddBdux-ENmx93bhGaMFMQoE6ieOHEM83IjYztMG5XWr9WXCNyA1uGfaxE2rWP9qtlcvPoOYfgN6ci_aqRSxMumE5s_yvdu62T4sRgFVhMGDTrKF4hsNtkxzk7-6yHbnQMbXGg1BrVNUA7nZp5aj6i7lp67WQpPN3HqPkM0hUFDodFluRq3WaibUehanTF0-ewI7h5X1LsMbv-lO1qi7d2QsYT2jxyfxqO17Iz9os0znyuhBTZk7naL3yyXiwLpwzk6yyLHiEotFHha8NPLbJ9qdLu_rKxVO8xQyG8xYErsUqSl-x9PArSo6oudhd59gqyVxuPsQSVcKz-trvEPExQEVUXHK8gYdpnK3HwpenhoR-GqCxXjWtG1qXiRC78se1u9qYaFloMPMnqfZjkeHQAN5y_f2-2PcaX76KvzDWCwqsuWuYk5mq4yJxc6vdF2w0A2oIpPJn-s9aIA-0G9zo2FneXGU6WH10y8G430F-E9YqNjfepj32J53HEfcQS5mcl_WV8V98etWQS7FserlTT__EW-eM1dI3UvVAHV5ptaSNhbqaI8OXBjfm0onnpmrN5QqBn3W9tQRO6q1A_H7UhRGVUb5IFb-TKC033oG4rYb__EwxNQ1rx3uaXqahbtxSDNnASci2j_jHRQOIp68krpmiZT_BTb80OYh8znnE-L-JdM25WYEoIi5WUKtjyl5P0Wi_-ZN8IkCqcCuruodh4wn9L5TjeyLeXYTcBN8ltxypEXbt6F5g"); - - const requestOptions = { - method: "GET", - headers: myHeaders, - redirect: "follow" - }; - - try { - const response = await fetch(`${process.env.BACKEND_URL}/api/user/1/accounts`, requestOptions); - const result = await response.json(); - setUserAccounts(result.result) - console.log(userAccounst); - - - } catch (error) { - console.error(error); - }; - - } - useEffect(() => { - getAccountsUser() - }, []) +export const Card = (props) => { return ( +
    -
    Nombre Cuenta
    +
    {props.name}
    -

    Saldo

    +

    {props.balance}

    -

    Moneda

    +

    {props.coin}

    -
    ) } \ No newline at end of file diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 444d153c94..5923e151b8 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from "react"; +import React, { useContext, useEffect,useState } from "react"; import { Sidebar } from "../component/sidebar"; import { Card } from "../component/card"; import "../../styles/container.css"; @@ -8,17 +8,40 @@ import { Link, useNavigate } from "react-router-dom"; export const PrincipalPage = () => { const { store, actions } = useContext(Context) + const [userAccounts, setUserAccounts] = useState([]) + let navigate = useNavigate(); + async function getAccountsUser() { + const myHeaders = new Headers(); + myHeaders.append("Cookie", ".Tunnels.Relay.WebForwarding.Cookies=CfDJ8Cs4yarcs6pKkdu0hlKHsZtLJEdvHSwRXZt17-888_aVJrmBrvu6RtYBoKMpKm9sEC-AxXi3sRWKgbFwf-p4hj2-84XL-87bz_Y0G7zB8iX-0y0kE4onkjgxSurJ2xjcvjN5j0SznVQpWIrQcTHHpW6OV8NIiNT91dMN_DBpqn7Ku0inf13JGgK7eJyftdxiu-_vwQHTzVbsiQN30MKU5UG7jMXXLPbrbXHRtaabX2EnUEHxglQZieSfFmtvDEvoGq26C0ixTRdmpVddBdux-ENmx93bhGaMFMQoE6ieOHEM83IjYztMG5XWr9WXCNyA1uGfaxE2rWP9qtlcvPoOYfgN6ci_aqRSxMumE5s_yvdu62T4sRgFVhMGDTrKF4hsNtkxzk7-6yHbnQMbXGg1BrVNUA7nZp5aj6i7lp67WQpPN3HqPkM0hUFDodFluRq3WaibUehanTF0-ewI7h5X1LsMbv-lO1qi7d2QsYT2jxyfxqO17Iz9os0znyuhBTZk7naL3yyXiwLpwzk6yyLHiEotFHha8NPLbJ9qdLu_rKxVO8xQyG8xYErsUqSl-x9PArSo6oudhd59gqyVxuPsQSVcKz-trvEPExQEVUXHK8gYdpnK3HwpenhoR-GqCxXjWtG1qXiRC78se1u9qYaFloMPMnqfZjkeHQAN5y_f2-2PcaX76KvzDWCwqsuWuYk5mq4yJxc6vdF2w0A2oIpPJn-s9aIA-0G9zo2FneXGU6WH10y8G430F-E9YqNjfepj32J53HEfcQS5mcl_WV8V98etWQS7FserlTT__EW-eM1dI3UvVAHV5ptaSNhbqaI8OXBjfm0onnpmrN5QqBn3W9tQRO6q1A_H7UhRGVUb5IFb-TKC033oG4rYb__EwxNQ1rx3uaXqahbtxSDNnASci2j_jHRQOIp68krpmiZT_BTb80OYh8znnE-L-JdM25WYEoIi5WUKtjyl5P0Wi_-ZN8IkCqcCuruodh4wn9L5TjeyLeXYTcBN8ltxypEXbt6F5g"); + const requestOptions = { + method: "GET", + headers: myHeaders, + redirect: "follow" + }; + + try { + const response = await fetch(`${process.env.BACKEND_URL}/api/user/${store.user.id}/accounts`, requestOptions); + const result = await response.json(); + setUserAccounts(result.result); + console.log(result.result); + + } catch (error) { + console.error(error); + + }; + } useEffect(() => { actions.verifyToken(); - actions.initializeStore() + actions.initializeStore(); + getAccountsUser() if (!store.auth) { navigate("/"); } }, []); - if (!store.auth) return null; + if (!store.auth) return

    borrar localstorage

    ; return (
    @@ -27,10 +50,12 @@ export const PrincipalPage = () => {

    Balance general

    - - - - + {userAccounts.map((item) => { + return ( + + ) + + })}
    diff --git a/src/instance/test.db b/src/instance/test.db index 16201faef98af2aef171bf94a4d8089eadd84369..799a11b637aef552cda56a418f2c73569fc5109c 100644 GIT binary patch delta 343 zcmZp8z}WDBae_3X=tLQ3M$wH4OZ3G!d0H6wPxDXVSK?dG=f!)SH-qOFPs?UOfh?Z- z5>8GA{YF-4Nk!@8#N5oBd_#xy+{DZrz2y8{6{92-Ln9S;b1!pO=b|(hGsh^OFpEn6 z#E>ep(yTyp*P@W1!bEdRKSRS}^I$#0U`KBw-+X_QtjN^LZa`ivNE;hdb~ zKSPL}?=?ga F003H3WXAvi delta 92 zcmV-i0Hgna-~oW(0gxL35s@520THoaq%RZ*4Eg{M%MX+fA`Y((N)6WyXbk$Z5fI`G yv-&&=0|p2WYyc0dvkX8=50Q` Date: Wed, 5 Mar 2025 23:32:33 +0000 Subject: [PATCH 31/45] se corrigieron los bugsdel login y se agregaron las rutas del formulario de registro --- src/front/js/component/login-form.js | 41 +++++++++++++++++----------- src/front/js/layout.js | 1 + src/front/js/pages/principalPage.js | 5 +++- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js index ec4fadfad1..836425bdf4 100644 --- a/src/front/js/component/login-form.js +++ b/src/front/js/component/login-form.js @@ -8,7 +8,11 @@ export const LoginForm = () => { const [password, setPassword] = useState("") const { store, actions } = useContext(Context) let navigate = useNavigate(); - + + const handleClick = () => { + // navigate("/registro") + } + async function handleSubmit(e) { e.preventDefault() await actions.login(email, password) @@ -18,6 +22,7 @@ export const LoginForm = () => { setInvalidAccount(false) } } + useEffect(() => { if (store.auth) { navigate("/cuentas"); @@ -30,23 +35,27 @@ export const LoginForm = () => { }, [store.auth]) return ( -
    -
    - - setEmail(e.target.value)} value={email} /> -
    -
    - - setPassword(e.target.value)} value={password} /> - {!invalidAccount ?
    Nunca compartiremos su correo electrónico con nadie más.
    - :
    Correo o Contraseña incorrectos
    } -
    + <> + +
    + + setEmail(e.target.value)} value={email} /> +
    +
    + + setPassword(e.target.value)} value={password} /> + {!invalidAccount ?
    Nunca compartiremos su correo electrónico con nadie más.
    + :
    Correo o Contraseña incorrectos
    } +
    +
    + +
    +
    +
    - -

    ¿Aun no estás registrado?

    - +
    - + ); }; \ No newline at end of file diff --git a/src/front/js/layout.js b/src/front/js/layout.js index 255675c128..d0499e63c0 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -27,6 +27,7 @@ const Layout = () => { } path="/" /> } path="/demo" /> } path="/cuentas" /> + {/* } path="/registro" /> */} } path="/single/:theid" /> Not found!} /> diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 444d153c94..1bf9f86761 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -18,7 +18,10 @@ export const PrincipalPage = () => { } }, []); - if (!store.auth) return null; + if (!store.auth){ + actions.logout() + navigate("/"); + } return (
    From 8713a1363e18bb1e38d2944c863f5b4c316ebb72 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Thu, 6 Mar 2025 09:51:33 +0000 Subject: [PATCH 32/45] =?UTF-8?q?se=20cre=C3=B3=20el=20boton=20y=20se=20ub?= =?UTF-8?q?ic=C3=B3=20en=20el=20lugar=20deaseado,=20tambien=20se=20cre?= =?UTF-8?q?=C3=B3=20el=20modal=20base=20para=20ser=20trabajado=20por=20otr?= =?UTF-8?q?o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/component/modal.js | 94 +++++++++++++++++++++++++++++ src/front/js/pages/principalPage.js | 2 + src/front/styles/modal.css | 7 +++ 3 files changed, 103 insertions(+) create mode 100644 src/front/js/component/modal.js create mode 100644 src/front/styles/modal.css diff --git a/src/front/js/component/modal.js b/src/front/js/component/modal.js new file mode 100644 index 0000000000..3a00b293bf --- /dev/null +++ b/src/front/js/component/modal.js @@ -0,0 +1,94 @@ +import React, { useState } from "react"; +import "../../styles/modal.css"; + +export const Modal = (props) => { + const [inputValue, setInputValue] = useState({ + articulo: "", + cant: 0, + type: "" + }); + + const addItem = () => { + if (inputValue.articulo != "" && inputValue.type != "" && inputValue.cant <= 10 && inputValue.cant >= 0) { + // props.setList([...props.list, inputValue]); + setInputValue({ + articulo: "", + cant: 0, + type: "" + }); + } else { + alert( + "------------------------INFORMACIÓN INCOMPLETA-------------------- RECUERDA ESCRIBIR UN NOMBRE, ESCOGER UN TIPO E INTRODUCIR UNA CANTIDAD DE MAXIMO 10 UNIDADES" + ); + } + }; + const handleChange = (e) => { + const { name, value } = e.target; + setInputValue({ ...inputValue, [name]: value }); + }; + + return ( + <> + + + + ); +} \ No newline at end of file diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 444d153c94..397e66f4b5 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -4,6 +4,7 @@ import { Card } from "../component/card"; import "../../styles/container.css"; import { Context } from "../store/appContext"; import { Link, useNavigate } from "react-router-dom"; +import { Modal } from "../component/modal"; export const PrincipalPage = () => { @@ -33,6 +34,7 @@ export const PrincipalPage = () => {
    +
    ); diff --git a/src/front/styles/modal.css b/src/front/styles/modal.css new file mode 100644 index 0000000000..2f0a40a849 --- /dev/null +++ b/src/front/styles/modal.css @@ -0,0 +1,7 @@ +.add-item { + position: fixed; + bottom: 20px; + right: 30px; + border-radius: 25%; + font-size: 1.8rem; +} \ No newline at end of file From e440ca86acc9622a85ba4c58e99fd3dc0764317e Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:13:59 +0000 Subject: [PATCH 33/45] =?UTF-8?q?se=20cre=C3=B3=20el=20modal=20y=20se=20co?= =?UTF-8?q?nect=C3=B3=20con=20el=20back=20para=20crear=20cuentas=20nuevas,?= =?UTF-8?q?=20tambien=20se=20integr=C3=B3=20validador=20de=20rutas=20para?= =?UTF-8?q?=20reusar=201=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/component/modal.js | 223 ++++++++++++++++++++++++++++++++ src/instance/test.db | Bin 28672 -> 28672 bytes 2 files changed, 223 insertions(+) create mode 100644 src/front/js/component/modal.js diff --git a/src/front/js/component/modal.js b/src/front/js/component/modal.js new file mode 100644 index 0000000000..9114bc0eb7 --- /dev/null +++ b/src/front/js/component/modal.js @@ -0,0 +1,223 @@ +import React, { useState, useContext } from "react"; +import { Context } from "../store/appContext"; +import { useLocation, useParams } from "react-router-dom"; + +// import "../../styles/modal.css"; + +export const Modal = (props) => { + const { store, actions } = useContext(Context) + const params = useParams() + const path = useLocation() + console.log("params", params); + console.log("path", path); + + const [inputValue, setInputValue] = useState({ + name: "", + balance: 0, + coin: "", + type: "" + }); + async function createAccount(params) { + const myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); + + const raw = JSON.stringify({ + "name": params.name, + "balance": params.balance, + "coin": params.coin, + "type": params.type + }); + + const requestOptions = { + method: "POST", + headers: myHeaders, + body: raw, + redirect: "follow" + }; + + try { + const response = await fetch(`${process.env.BACKEND_URL}/api/${store.user.id}/new-account`, requestOptions); + const result = await response.text(); + console.log(result) + } catch (error) { + console.error(error); + }; + + } + const addItem = () => { + if (inputValue.name.length != 0 && inputValue.type != "") { + // props.setList([...props.list, inputValue]); + createAccount(inputValue) + setInputValue({ + name: "", + balance: 0, + coin: "", + type: "" + }); + alert("Se ingresó todo correctamente") + } else { + alert( + "------------------------INFORMACIÓN INCOMPLETA-------------------- RECUERDA ESCRIBIR UN NOMBRE, ESCOGER UN TIPO E INTRODUCIR UNA CANTIDAD DE MAXIMO 10 UNIDADES" + ); + } + }; + const handleChange = (e) => { + const { name, value } = e.target; + setInputValue({ ...inputValue, [name]: value }); + }; + + return ( + <> + + {path.pathname === "/cuentas" ? + + : } + + ); +} \ No newline at end of file diff --git a/src/instance/test.db b/src/instance/test.db index 3c57fe3f90d5f85acb5caa89208e3a8589a4fbb8..ad2b6518a4e1b77a2241d5fee368e355c5eb2e90 100644 GIT binary patch delta 110 zcmZp8z}WDBae_3X)I=F)MyZVnOZ<75_zy7f-{(KT@5t}CSx~@)pK<@h#%c#yCUyo! zc}8Jb?%>3{lEl1})FL_8&>+{e)Z~)PvV3_)ptLBXur{}oQ<$R&P^LJsEHf{?ST`{_ MIlnZoWKn_u0L!f(pa1{> delta 233 zcmZp8z}WDBae_3X$V3@uMv;vPOZ<6Q_#GMeFY_T99y3sjF?KE3)ax<<)f-|} ZukL`wd~HUcdVMB&Sqvv(F?~^j006;HO4|Sc From 5697d0bfbfe91d424e3f5b1c6413f5e17f73cf62 Mon Sep 17 00:00:00 2001 From: LauraPostigo <166739141+LauraPostigo@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:50:32 +0000 Subject: [PATCH 34/45] registro de usuario con conexion a back --- src/front/js/component/signup-form.js | 74 +++++++++++++++++++++++++++ src/front/js/layout.js | 2 + src/front/js/pages/login.js | 5 +- src/front/js/store/flux.js | 25 +++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/front/js/component/signup-form.js diff --git a/src/front/js/component/signup-form.js b/src/front/js/component/signup-form.js new file mode 100644 index 0000000000..bff6036ce4 --- /dev/null +++ b/src/front/js/component/signup-form.js @@ -0,0 +1,74 @@ +import React, { useState, useContext, } from "react"; +import { Context } from "../store/appContext"; +import { Link, useNavigate } from "react-router-dom"; + + +export const SignupForm = () => { + const { actions } = useContext(Context); +// definir nuevo estado para confirmar y debajo boton para volver al home. darle un nombre ale estado + // const [itsOk, setItsOk] =useState(false) + + let navigate = useNavigate(); + const [formData, setFormData] = useState({ + first_name: "", + last_name: "", + birthdate: "", + country: "", + email: "", + password: "" + }); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData({ ...formData, [name]: value }); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + actions.registerUser(formData); + }; + + const handleOnclic = (e) => { + navigate("/"); + + } + + return ( +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + +
    +
    + + ); + +}; + diff --git a/src/front/js/layout.js b/src/front/js/layout.js index 255675c128..181741b21a 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -6,6 +6,7 @@ import { BackendURL } from "./component/backendURL"; import { Home } from "./pages/home"; import { Demo } from "./pages/demo"; import { Single } from "./pages/single"; +import { SignupForm } from "./component/signup-form"; import injectContext from "./store/appContext"; import { Navbar } from "./component/navbar"; @@ -27,6 +28,7 @@ const Layout = () => { } path="/" /> } path="/demo" /> } path="/cuentas" /> + } path="/signup" /> } path="/single/:theid" /> Not found!} /> diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js index fff3fb0855..a47c6a37b9 100644 --- a/src/front/js/pages/login.js +++ b/src/front/js/pages/login.js @@ -1,7 +1,8 @@ import React from "react"; -import { LoginForm } from "../component/login-form"; +import { SignupForm } from "../component/signup-form"; import "../../styles/login.css"; + export const Login = () => { return ( @@ -12,7 +13,7 @@ export const Login = () => {

    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

    - +
    diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index dc7c86f80d..c6b4d13012 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -141,6 +141,31 @@ const getState = ({ getStore, getActions, setStore }) => { getActions().getPrivate(); } }, + + // registro usuario form + registerUser: async (formData) => { + try { + const response = await fetch(`${process.env.BACKEND_URL}/api/signup`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData) + }); + const result = await response.json(); + + if (response.status === 201) { + localStorage.setItem("userLogged", JSON.stringify(result.user)); + setStore({ user: result.user, auth: true }); + + return { success: true, message: "Successfully registered" }; + } else { + return { success: false, message: result.msg || "Registration error. Please try again" }; + } + } catch (error) { + console.error("Failed to connect to the server", error); + return { success: false, message: "Error en la conexión con el servidor." }; + } + }, + // fin registro usuario } }; }; From e638ce38fb7e2390505747a1d184ecab7d8bc39d Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:03:29 +0000 Subject: [PATCH 35/45] =?UTF-8?q?se=20conect=C3=B3=20con=20el=20form=20de?= =?UTF-8?q?=20registro=20y=20se=20agregaron=20correcciones=20de=20errores?= =?UTF-8?q?=20con=20el=20local=20storage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/component/login-form.js | 2 +- src/front/js/layout.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/front/js/component/login-form.js b/src/front/js/component/login-form.js index 836425bdf4..8fe533186a 100644 --- a/src/front/js/component/login-form.js +++ b/src/front/js/component/login-form.js @@ -10,7 +10,7 @@ export const LoginForm = () => { let navigate = useNavigate(); const handleClick = () => { - // navigate("/registro") + navigate("/registro") } async function handleSubmit(e) { diff --git a/src/front/js/layout.js b/src/front/js/layout.js index 181741b21a..f286996a17 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -28,7 +28,7 @@ const Layout = () => { } path="/" /> } path="/demo" /> } path="/cuentas" /> - } path="/signup" /> + } path="/registro" /> } path="/single/:theid" /> Not found!} /> From bcd24d6a1741eba805176a3ea270645741364660 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:08:55 +0000 Subject: [PATCH 36/45] bugx fix --- src/front/js/component/card.js | 32 ++++++++++++++++++++--------- src/front/js/pages/principalPage.js | 2 +- src/front/styles/card.css | 3 +++ 3 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 src/front/styles/card.css diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js index cc26a2c4ef..8862a4b6bc 100644 --- a/src/front/js/component/card.js +++ b/src/front/js/component/card.js @@ -1,23 +1,35 @@ -import React from "react"; +import React, { useState } from "react"; + +export const Card = (props) => { + const [showBalance, setShowBalance] = useState(true) + + const toggleBalance = () => { + let toggle = !showBalance + setShowBalance(toggle); + console.log(toggle); + + } + + -export const Card = (props) => { return ( - +
    -
    +
    {props.name}
    +
    +
    -

    {props.balance}

    - - +

    {showBalance ? props.balance : "****"}

    + {showBalance ? : }

    {props.coin}

    -
    - Button +
    ) -} \ No newline at end of file +} diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index 5923e151b8..8fde29e664 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -52,7 +52,7 @@ export const PrincipalPage = () => {
    {userAccounts.map((item) => { return ( - + ) })} diff --git a/src/front/styles/card.css b/src/front/styles/card.css new file mode 100644 index 0000000000..440e0237a8 --- /dev/null +++ b/src/front/styles/card.css @@ -0,0 +1,3 @@ +.balance { + transition: all 0.3s ease-in-out; +} From 858788413f4c2979c1df01b65e897b7019a6bf6a Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:21:30 +0000 Subject: [PATCH 37/45] merge realizado correctamente --- src/instance/test.db | Bin 28672 -> 28672 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/instance/test.db b/src/instance/test.db index 799a11b637aef552cda56a418f2c73569fc5109c..ca50dc19e63960035b1938e6c194bde670ef5024 100644 GIT binary patch delta 229 zcmZp8z}WDBae_3X*hCp;MzM_vOY|kUc!U`EPxDXVSK?dG=f!)SH-qOFPYaLGWW=S>eGIMMaju`i15O z`juuC&W?Vip^<^6spXF4&cOl3iAjOpNqSB$$sQ?biN&c&RRMYdAkzX3EiDan4Ge)O zEHy8+sx&nxv4oS8LBEkzT2fIuIdStun_^}jE`ECk{?+_p{5SaRH}g5@^NWiy8!}?L Lhih|~|0)FlJjz2Q delta 76 zcmV-S0JHyq-~oW(0gxL36_Ff60Tr=eq%Rf;41@p=%MX+fA`Y((N)6WyXbk!cgtHM4 iY7Dc4Jb(iV2@h5P533JW57@I1KrauGkQuX9PpTkypcg6t From ef1fb1c505f9cfa22263d6f7242078afecc8add0 Mon Sep 17 00:00:00 2001 From: Luis Malizia Date: Thu, 6 Mar 2025 15:50:54 +0000 Subject: [PATCH 38/45] se creo la logica para hacer la suma de los balances y mostrarlos en uno general, ademas se le agrego el boton de esconder el balance --- src/front/js/component/balanceGeneral.js | 62 ++++++------------------ src/front/js/pages/principalPage.js | 23 +++++---- 2 files changed, 28 insertions(+), 57 deletions(-) diff --git a/src/front/js/component/balanceGeneral.js b/src/front/js/component/balanceGeneral.js index 3f145aacdb..8243129535 100644 --- a/src/front/js/component/balanceGeneral.js +++ b/src/front/js/component/balanceGeneral.js @@ -1,54 +1,22 @@ import React, { useState, useEffect } from "react"; -import { useContext } from "react"; -import { Context } from "../store/appContext"; -export const GeneralBalance = () => { - const [balance, setBalance] = useState(0); +export const GeneralBalance = (props) => { + + const [showBalance, setShowBalance] = useState(true) + + const toggleBalance = () => { + let toggle = !showBalance + setShowBalance(toggle); + console.log(toggle); + + } - const {store,actions} = useContext(Context) - - const generalBalance = async () => { - const myHeaders = new Headers(); - myHeaders.append( - "Cookie", - ".Tunnels.Relay.WebForwarding.Cookies=CfDJ8Cs4yar..." - ); - - const requestOptions = { - method: "GET", - headers: myHeaders, - redirect: "follow", - }; - - try { - const response = await fetch - (`${process.env.BACKEND_URL}/api/user/${store.user.id}/accounts`, - requestOptions - ); - const data = await response.json(); -// console.log(data[0].balance); -setBalance(data.result[0].balance); - - - - // Ajustar según la estructura real de la API - // if (data && Array.isArray(data.valores)) { - // const totalBalance = data.valores.reduce((acc, num) => acc + num, 0); - // setBalance(totalBalance); - // } else { - // console.error("Estructura de datos inesperada:", data); - // setError("Formato de datos incorrecto"); - // } - } catch (error) { - console.error("Error al obtener el balance:", error); - } - }; - - useEffect(() => { - generalBalance() - }, []); return ( -

    {balance} euros

    +
    +

    {showBalance ? props.balance : "****"} euros

    + {showBalance ? : } +
    + ); }; diff --git a/src/front/js/pages/principalPage.js b/src/front/js/pages/principalPage.js index e7fa72129d..74bae64a23 100644 --- a/src/front/js/pages/principalPage.js +++ b/src/front/js/pages/principalPage.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect,useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { Sidebar } from "../component/sidebar"; import { Card } from "../component/card"; import { GeneralBalance } from "../component/balanceGeneral"; @@ -11,6 +11,7 @@ import { Modal } from "../component/modal"; export const PrincipalPage = () => { const { store, actions } = useContext(Context) const [userAccounts, setUserAccounts] = useState([]) + const totalBalance = userAccounts.reduce((acc, item) => acc + item.balance, 0); let navigate = useNavigate(); async function getAccountsUser() { @@ -26,12 +27,12 @@ export const PrincipalPage = () => { try { const response = await fetch(`${process.env.BACKEND_URL}/api/user/${store.user.id}/accounts`, requestOptions); const result = await response.json(); - setUserAccounts(result.result); + setUserAccounts(result.result); console.log(result.result); - + } catch (error) { console.error(error); - + }; } useEffect(() => { @@ -43,28 +44,30 @@ export const PrincipalPage = () => { } }, []); - if (!store.auth){ + if (!store.auth) { actions.logout() navigate("/"); - } - + } + return (

    Balance general

    - +
    + +
    {userAccounts.map((item) => { return ( - + ) })}
    - +
    ); From c2a9fe039f0634ea844daa9afb2b54f86b18d822 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Fri, 7 Mar 2025 00:25:53 +0000 Subject: [PATCH 39/45] =?UTF-8?q?creaci=C3=B3n=20de=20la=20p=C3=A1gina=204?= =?UTF-8?q?04?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/img/404.png | Bin 0 -> 112621 bytes src/front/js/layout.js | 3 ++- src/front/js/pages/notFound.js | 15 +++++++++++++ src/front/styles/not-found.css | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/front/img/404.png create mode 100644 src/front/js/pages/notFound.js create mode 100644 src/front/styles/not-found.css diff --git a/src/front/img/404.png b/src/front/img/404.png new file mode 100644 index 0000000000000000000000000000000000000000..9686ff9eca5e03ea73489ea0ad727eda7a52954c GIT binary patch literal 112621 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�Wpc%U-)BPdGYom5M6H087m3KrV#FEZ*I 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 ypu2hee`2f { @@ -30,7 +31,7 @@ const Layout = () => { } path="/cuentas" /> } path="/registro" /> } path="/single/:theid" /> - Not found!} /> + } path="*" /> ); diff --git a/src/front/js/pages/notFound.js b/src/front/js/pages/notFound.js new file mode 100644 index 0000000000..4afc6c3da1 --- /dev/null +++ b/src/front/js/pages/notFound.js @@ -0,0 +1,15 @@ +import React from "react"; +import "../../styles/not-found.css"; +import { Link } from "react-router-dom"; +import errorImage from "../../img/404.png"; // Asegúrate de tener esta imagen en tu proyecto + +export const NotFound = () => { + return ( +
    + 404 Not Found +

    404

    +

    Página no encontrada. Parece que te perdiste.

    + Volver al inicio +
    + ); +}; \ No newline at end of file diff --git a/src/front/styles/not-found.css b/src/front/styles/not-found.css new file mode 100644 index 0000000000..adacbef77f --- /dev/null +++ b/src/front/styles/not-found.css @@ -0,0 +1,38 @@ +/* NotFound.css */ +.error-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + text-align: center; + background-color: #f8f9fa; + color: #333; +} + +.error-image { + width: 300px; +} + +.not-found-title { + font-size: 5rem; + margin: 10px 0; +} + +.not-found-paragraph { + font-size: 1.2rem; + margin-bottom: 20px; +} + +.btn-home { + text-decoration: none; + padding: 10px 20px; + background-color: #007bff; + color: white; + border-radius: 5px; + transition: background 0.3s ease; +} + +.btn-home:hover { + background-color: #0056b3; +} From f65c8a4bc4f1775b77c960f6e9b177d6fecb7bd9 Mon Sep 17 00:00:00 2001 From: Camilo Cortes <100862481+camilocortes27@users.noreply.github.com> Date: Fri, 7 Mar 2025 08:56:00 +0000 Subject: [PATCH 40/45] estilos casi terminados --- src/front/js/component/card.js | 17 +++++++++-------- src/instance/test.db | Bin 28672 -> 28672 bytes 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/front/js/component/card.js b/src/front/js/component/card.js index 8862a4b6bc..e1ce99c7e4 100644 --- a/src/front/js/component/card.js +++ b/src/front/js/component/card.js @@ -6,25 +6,26 @@ export const Card = (props) => { const toggleBalance = () => { let toggle = !showBalance setShowBalance(toggle); - console.log(toggle); - } - - - return ( -
    +
    -
    {props.name}
    +
    +
    {props.name}
    +
    +

    Ultimo movimiento

    +

    Detalle ultimo movimiento

    +
    +

    {showBalance ? props.balance : "****"}

    {showBalance ? : } +

    {props.coin}

    -

    {props.coin}

    diff --git a/src/instance/test.db b/src/instance/test.db index c8fd7562e6169ed66b9955c5ef4002bd02113aa3..63dde860d7a277452e08c6687d5bde6def4393f6 100644 GIT binary patch delta 283 zcmZp8z}WDBae_3X>_i!7M%j%COY~)UxJ?=OPxDXVSK?dG=f!)SH-qOFPdASY_Zn`~ z&4L1=-1VhAoDBMntkTko(#eTMxv4q%4(YjxnK^pN`MD}aNh*d$D(UHIMFkOsriNw( zWhItAnYn4krI!B17A5ZP?irEE`Fe#}6^WiDo*6|M*(Lem{)IWY8O~6{auahht1=Uf z3=9o)4Gnb-3{#5>5)W@o%;V-{Fl=NI-@L^-fQ5^PUzmY^HUABM;mte_to&l4%=(NN zuHl)i>3^Gtg};k||33c#{w4ffKp&X!Pj-oSlagRzXJ8a%6qe;lN-9g_at#e~O-oHK N$t=s?yd*wG005TzRbc=C delta 107 zcmV-x0F?iL-~oW(0gxL38<8AC0UNPkq%Rr^3# Date: Fri, 7 Mar 2025 09:09:14 +0000 Subject: [PATCH 41/45] pull --- src/front/js/component/signup-form.js | 74 ++++++++++++++------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/front/js/component/signup-form.js b/src/front/js/component/signup-form.js index bff6036ce4..1784ee8172 100644 --- a/src/front/js/component/signup-form.js +++ b/src/front/js/component/signup-form.js @@ -5,7 +5,7 @@ import { Link, useNavigate } from "react-router-dom"; export const SignupForm = () => { const { actions } = useContext(Context); -// definir nuevo estado para confirmar y debajo boton para volver al home. darle un nombre ale estado + // definir nuevo estado para confirmar y debajo boton para volver al home. darle un nombre ale estado // const [itsOk, setItsOk] =useState(false) let navigate = useNavigate(); @@ -30,43 +30,45 @@ export const SignupForm = () => { const handleOnclic = (e) => { navigate("/"); - + } - + return ( -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    - -
    -
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + +
    +
    +
    ); From 67ff59fa71fe6dc8a4f38b1a1947c3aa74df5913 Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Fri, 7 Mar 2025 11:54:28 +0000 Subject: [PATCH 42/45] =?UTF-8?q?se=20borr=C3=B3=20de=20la=20abse=20de=20d?= =?UTF-8?q?atos=20las=20opciones=20de=20birthdate=20y=20country?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/versions/e63597c30f77_.py | 34 ++++++++++++++++++++++++++++ src/api/models.py | 4 ---- 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/e63597c30f77_.py diff --git a/migrations/versions/e63597c30f77_.py b/migrations/versions/e63597c30f77_.py new file mode 100644 index 0000000000..54798a1138 --- /dev/null +++ b/migrations/versions/e63597c30f77_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: e63597c30f77 +Revises: 30d819e19461 +Create Date: 2025-03-07 11:53:02.566692 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e63597c30f77' +down_revision = '30d819e19461' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_column('country') + batch_op.drop_column('birthdate') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('birthdate', sa.VARCHAR(length=80), autoincrement=False, nullable=False)) + batch_op.add_column(sa.Column('country', sa.VARCHAR(length=120), autoincrement=False, nullable=False)) + + # ### end Alembic commands ### diff --git a/src/api/models.py b/src/api/models.py index 5e633206ec..f42c4fd0e3 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -12,8 +12,6 @@ class User(db.Model): password: Mapped[str] = mapped_column(String(256),nullable=False) first_name: Mapped[str] = mapped_column(String(120),nullable=False) last_name: Mapped[str] = mapped_column(String(120),nullable=False) - birthdate: Mapped[str] = mapped_column(String(80),nullable=False) - country: Mapped[str] = mapped_column(String(120),nullable=False) accounts: Mapped[List["Accounts"]] = relationship() def serialize(self): @@ -22,8 +20,6 @@ def serialize(self): "email": self.email, "first_name": self.first_name, "last_name": self.last_name, - "birthdate": self.birthdate, - "country": self.country, # do not serialize the password, its a security breach } From edcc75f7122426d77593888349a1db16276749a7 Mon Sep 17 00:00:00 2001 From: LauraPostigo <166739141+LauraPostigo@users.noreply.github.com> Date: Fri, 7 Mar 2025 12:08:46 +0000 Subject: [PATCH 43/45] eliminado birtdate y country del back --- src/api/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index bb4cf0c70b..cb474c8166 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -128,11 +128,11 @@ def get_one_account_to_one_user(user_id): def signup(): body = request.json # para manejo de errores poner exactamente el nombre del front igual en los campos entre parentesis (esperar a que se haga el front) - if not body or not body.get("email") or not body.get("password") or not body.get("last_name")or not body.get("first_name")or not body.get("birthdate")or not body.get("country"): + if not body or not body.get("email") or not body.get("password") or not body.get("last_name")or not body.get("first_name"): return jsonify({"msg": "missing fields"}), 400 hashe_password = bcrypt.generate_password_hash(body["password"]).decode("utf-8") # encajar con los nombres del front estos (solo los que estan entre comillas) - new_user = User(email = body["email"],password=hashe_password, last_name= body["last_name"],first_name= body["first_name"],birthdate= body["birthdate"],country= body["country"]) + new_user = User(email = body["email"],password=hashe_password, last_name= body["last_name"],first_name= body["first_name"]) db.session.add(new_user) db.session.commit() return jsonify({"msg": "user created"}), 201 \ No newline at end of file From dfdf6aff3530525aed0f92e054e59ddb7262ea5a Mon Sep 17 00:00:00 2001 From: Pablo Querales <119375308+PabloQuerales@users.noreply.github.com> Date: Fri, 7 Mar 2025 12:38:28 +0000 Subject: [PATCH 44/45] =?UTF-8?q?se=20elmin=C3=B3=20el=20seteo=20del=20loc?= =?UTF-8?q?al=20storage=20del=20archivo=20flux=20que=20redirig=C3=ADa=20al?= =?UTF-8?q?=20usuario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/js/store/flux.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index c6b4d13012..6c0b244797 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -153,9 +153,6 @@ const getState = ({ getStore, getActions, setStore }) => { const result = await response.json(); if (response.status === 201) { - localStorage.setItem("userLogged", JSON.stringify(result.user)); - setStore({ user: result.user, auth: true }); - return { success: true, message: "Successfully registered" }; } else { return { success: false, message: result.msg || "Registration error. Please try again" }; From eca1799fae73efb0138b25d1503be3b73e416465 Mon Sep 17 00:00:00 2001 From: LauraPostigo <166739141+LauraPostigo@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:07:02 +0000 Subject: [PATCH 45/45] acomodado el form --- src/front/js/component/signup-form.js | 67 ++++++++++++--------------- src/front/js/store/flux.js | 1 + 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/front/js/component/signup-form.js b/src/front/js/component/signup-form.js index 1784ee8172..47b873a446 100644 --- a/src/front/js/component/signup-form.js +++ b/src/front/js/component/signup-form.js @@ -12,8 +12,6 @@ export const SignupForm = () => { const [formData, setFormData] = useState({ first_name: "", last_name: "", - birthdate: "", - country: "", email: "", password: "" }); @@ -34,43 +32,36 @@ export const SignupForm = () => { } return ( -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    - -
    -
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + +
    +
    +
    - - ); + ); }; diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index 6c0b244797..3d14542e2d 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -151,6 +151,7 @@ const getState = ({ getStore, getActions, setStore }) => { body: JSON.stringify(formData) }); const result = await response.json(); + console.log(result) if (response.status === 201) { return { success: true, message: "Successfully registered" };