diff --git a/.gitignore b/.gitignore index 80704f4378..12e3b32354 100755 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ database.database database.db diagram.png __pycache__/ +migrations \ No newline at end of file diff --git a/Pipfile b/Pipfile index 4d377014ae..f75fb3d98a 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +flask-bcrypt = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index d9e474e972..7f919a3beb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ffbfb32d0afa5e4bcaba5c2d08c81381a97abd90f22284d2b76647365df5dc50" + "sha256": "9c612f7eebcf717533779dc02b40fce5eeaf66bfab71c81d166828128c272240" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,75 @@ "markers": "python_version >= '3.10'", "version": "==1.17.1" }, + "bcrypt": { + "hashes": [ + "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", + "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", + "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", + "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", + "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", + "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", + "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", + "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", + "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", + "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", + "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", + "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", + "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", + "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", + "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", + "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", + "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", + "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", + "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", + "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", + "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", + "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", + "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", + "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", + "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", + "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", + "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", + "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", + "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", + "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", + "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", + "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", + "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", + "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", + "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", + "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", + "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", + "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", + "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", + "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", + "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", + "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", + "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", + "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", + "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", + "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", + "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", + "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", + "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", + "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", + "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", + "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", + "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", + "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", + "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", + "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", + "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", + "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", + "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", + "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", + "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", + "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", + "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b" + ], + "markers": "python_version >= '3.8'", + "version": "==5.0.0" + }, "blinker": { "hashes": [ "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", @@ -42,11 +111,11 @@ }, "click": { "hashes": [ - "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", - "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" + "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", + "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" ], "markers": "python_version >= '3.10'", - "version": "==8.3.0" + "version": "==8.3.1" }, "cloudinary": { "hashes": [ @@ -58,12 +127,12 @@ }, "flask": { "hashes": [ - "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", - "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c" + "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", + "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.1.2" + "version": "==3.1.3" }, "flask-admin": { "hashes": [ @@ -74,6 +143,14 @@ "markers": "python_version >= '3.10'", "version": "==2.0.0" }, + "flask-bcrypt": { + "hashes": [ + "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a", + "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369" + ], + "index": "pypi", + "version": "==1.0.1" + }, "flask-cors": { "hashes": [ "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", @@ -575,11 +652,11 @@ }, "werkzeug": { "hashes": [ - "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", - "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" + "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", + "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131" ], "markers": "python_version >= '3.9'", - "version": "==3.1.3" + "version": "==3.1.6" }, "wtforms": { "hashes": [ diff --git a/docs/assets/logo.jpg b/docs/assets/logo.jpg new file mode 100644 index 0000000000..917d074903 Binary files /dev/null and b/docs/assets/logo.jpg differ diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py deleted file mode 100644 index 88964176f1..0000000000 --- a/migrations/versions/0763d677d453_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 0763d677d453 -Revises: -Create Date: 2025-02-25 14:47:16.337069 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0763d677d453' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user') - # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..398b3c017c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -877,19 +877,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -997,14 +984,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1307,14 +1286,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3915,29 +3886,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -4074,35 +4022,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/terser": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.38.1.tgz", - "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4944,18 +4863,6 @@ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true }, - "@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -5044,14 +4951,6 @@ "@babel/types": "^7.20.7" } }, - "@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -5252,14 +5151,6 @@ "update-browserslist-db": "^1.1.1" } }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -6982,28 +6873,6 @@ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true - } - } - }, "string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -7094,30 +6963,6 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "terser": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.38.1.tgz", - "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - } - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/src/api/commands.py b/src/api/commands.py index 19806164d3..cff1a785fb 100644 --- a/src/api/commands.py +++ b/src/api/commands.py @@ -1,34 +1,34 @@ -import click -from api.models import db, User +# import click +# from api.models import db -""" -In this file, you can add as many commands as you want using the @app.cli.command decorator -Flask commands are usefull to run cronjobs or tasks outside of the API but sill in integration -with youy database, for example: Import the price of bitcoin every night as 12am -""" -def setup_commands(app): +# """ +# In this file, you can add as many commands as you want using the @app.cli.command decorator +# Flask commands are usefull to run cronjobs or tasks outside of the API but sill in integration +# with youy database, for example: Import the price of bitcoin every night as 12am +# """ +# def setup_commands(app): - """ - This is an example command "insert-test-users" that you can run from the command line - by typing: $ flask insert-test-users 5 - Note: 5 is the number of users to add - """ - @app.cli.command("insert-test-users") # name of our command - @click.argument("count") # argument of out command - def insert_test_users(count): - print("Creating test users") - for x in range(1, int(count) + 1): - user = User() - user.email = "test_user" + str(x) + "@test.com" - user.password = "123456" - user.is_active = True - db.session.add(user) - db.session.commit() - print("User: ", user.email, " created.") +# """ +# This is an example command "insert-test-users" that you can run from the command line +# by typing: $ flask insert-test-users 5 +# Note: 5 is the number of users to add +# """ +# @app.cli.command("insert-test-users") # name of our command +# @click.argument("count") # argument of out command +# def insert_test_users(count): +# print("Creating test users") +# for x in range(1, int(count) + 1): +# user = User() +# user.email = "test_user" + str(x) + "@test.com" +# user.password = "123456" +# user.is_active = True +# db.session.add(user) +# db.session.commit() +# print("User: ", user.email, " created.") - print("All test users created") +# print("All test users created") - @app.cli.command("insert-test-data") - def insert_test_data(): - pass \ No newline at end of file +# @app.cli.command("insert-test-data") +# def insert_test_data(): +# pass \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..644454bc79 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,257 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Table, Column, Integer, String, ForeignKey, Enum +from sqlalchemy.orm import Mapped, mapped_column, relationship +from flask_bcrypt import check_password_hash, generate_password_hash +import enum db = SQLAlchemy() -class User(db.Model): +evento_estudiantes = Table( + "evento_estudiantes", + db.Model.metadata, + Column("evento_id", Integer, ForeignKey( + "eventos.evento_id"), primary_key=True), + Column("alumnos_id", Integer, ForeignKey( + "estudiantes.id"), primary_key=True), +) + +tutor_estudiantes = Table( + "tutor_estudiantes", + db.Model.metadata, + Column("estudiantes_id", Integer, ForeignKey( + "estudiantes.id"), primary_key=True), + Column("tutor_id", Integer, ForeignKey( + "tutor_legal.id"), primary_key=True), + Column("parentesco", String(255)) +) + + +class SuperAdmin(db.Model): + __tablename__ = "super_admin" + id: Mapped[int] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + password: Mapped[str] = mapped_column(String(255), nullable=False) + nombre_colegio: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) + rol_id: Mapped[int] = mapped_column(Integer, default=1, nullable=False) + + aulas = relationship("Aula", back_populates="colegio", foreign_keys="[Aula.colegio_id]") + + def serialize(self): + return { + "id": self.id, + "email": self.email, + "nombre_colegio": self.nombre_colegio, + "rol_id": self.rol_id + } + + def set_password(self,password): + self.password = generate_password_hash(password).decode('utf-8') + + def check_password(self,password): + return check_password_hash(self.password,password) + +class TutorLegal(db.Model): + __tablename__ = "tutor_legal" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(120), nullable=True) + email: Mapped[str] = mapped_column(String(120), nullable=True) + password: Mapped[str] = mapped_column(String(120), nullable=True) + telephone: Mapped[str] = mapped_column(String(80), nullable=True) + rol_id: Mapped[int] = mapped_column(Integer, default=3, nullable=False) + estudiantes = relationship( + "Estudiantes", + secondary=tutor_estudiantes, + back_populates="tutores" + ) def serialize(self): return { "id": self.id, + "name": self.name, "email": self.email, - # do not serialize the password, its a security breach - } \ No newline at end of file + "telephone": self.telephone, + "rol_id": self.rol_id + } + def set_password(self,password): + self.password = generate_password_hash(password).decode('utf-8') + + def check_password(self,password): + return check_password_hash(self.password,password) + + +class Profesor(db.Model): + __tablename__ = "profesor" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(120), nullable=True) + email: Mapped[str] = mapped_column(String(120), nullable=True) + password: Mapped[str] = mapped_column(String(120), nullable=True) + telephone: Mapped[str] = mapped_column(String(120), nullable=True) + rol_id: Mapped[int] = mapped_column(Integer, default=2, nullable=False) + + estudiantes = relationship("Estudiantes", back_populates="profesor") + eventos = relationship("Eventos", back_populates="profesor") + aulas = relationship("Aula", back_populates="profesor") + + + + def set_password(self,password): + self.password = generate_password_hash(password).decode('utf-8') + + def check_password(self,password): + return check_password_hash(self.password, password) + + + def serialize(self): + return { + "id": self.id, + "name": self.name, + "email": self.email, + "telephone": self.telephone, + "rol_id": self.rol_id + } + + +class Aula(db.Model): + __tablename__ = "aula" + + aula_id: Mapped[int] = mapped_column(primary_key=True) + curso: Mapped[str] = mapped_column(String(80), nullable=True) + clase: Mapped[str] = mapped_column(String(40), nullable=True) + + profesor_id: Mapped[int] = mapped_column( + ForeignKey("profesor.id"), + nullable=True + ) + colegio_id: Mapped[int] = mapped_column( + ForeignKey("super_admin.id"), + nullable=True + ) + + profesor = relationship("Profesor", back_populates="aulas") + colegio = relationship("SuperAdmin", back_populates="aulas", foreign_keys=[colegio_id]) + estudiantes = relationship("Estudiantes", back_populates="aula") + + def serialize(self): + return { + "aula_id": self.aula_id, + "curso": self.curso, + "clase": self.clase, + "profesor_id": self.profesor_id, + "estudiantes": [estudiante.serialize() for estudiante in self.estudiantes] + } + +class Estudiantes(db.Model): + __tablename__ = "estudiantes" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(120), nullable=True) + + profesor_id: Mapped[int] = mapped_column( + ForeignKey("profesor.id"), + nullable=True + ) + aula_id: Mapped[int] = mapped_column( + ForeignKey("aula.aula_id"), + nullable=True + ) + profesor = relationship("Profesor", back_populates="estudiantes") + aula = relationship("Aula", back_populates="estudiantes") + eventos = relationship( + "Eventos", + secondary=evento_estudiantes, + back_populates="alumnos" + ) + tutores = relationship( + "TutorLegal", + secondary=tutor_estudiantes, + back_populates="estudiantes" + ) + calificaciones = relationship( + "Calificaciones", back_populates="estudiante") + + def serialize(self): + return { + "id": self.id, + "name": self.name, + "profesor_id": self.profesor_id + } + + +class Asignaturas(db.Model): + __tablename__ = "asignaturas" + + asignatura_id: Mapped[int] = mapped_column(primary_key=True) + nombre_asignatura: Mapped[str] = mapped_column(String(120), nullable=True) + + calificaciones = relationship( + "Calificaciones", back_populates="asignatura") + + def serialize(self): + return { + "nombre_asignatura": self.nombre_asignatura, + "asignatura_id": self.asignatura_id, + } + + +class Calificaciones(db.Model): + __tablename__ = "calificaciones" + + calificacion_id: Mapped[int] = mapped_column(primary_key=True) + calificacion: Mapped[int] = mapped_column(nullable=True) + + asignatura_id: Mapped[int] = mapped_column( + ForeignKey("asignaturas.asignatura_id"), + nullable=True + ) + estudiante_id: Mapped[int] = mapped_column( + ForeignKey("estudiantes.id"), + nullable=True + ) + asignatura = relationship("Asignaturas", back_populates="calificaciones") + estudiante = relationship("Estudiantes", back_populates="calificaciones") + + def serialize(self): + return { + "calificacion_id": self.calificacion_id, + "calificacion": self.calificacion, + } + + +class tipo_evento(enum.Enum): + EXCURSION = "excursion" + EXAMEN = "examen" + REUNION = "reunion" + EVENTO_SOLIDARIO = "evento solidario" + + +class Eventos(db.Model): + __tablename__ = "eventos" + + evento_id: Mapped[int] = mapped_column(primary_key=True) + nombre_evento: Mapped[str] = mapped_column(String(80), nullable=True) + localizacion: Mapped[str] = mapped_column(String(80), nullable=True) + tipo_de_evento: Mapped[tipo_evento] = mapped_column( + Enum(tipo_evento), nullable=False) + profesor_id: Mapped[int] = mapped_column( + ForeignKey("profesor.id"), + nullable=True + ) + + profesor = relationship("Profesor", back_populates="eventos") + alumnos = relationship( + "Estudiantes", + secondary=evento_estudiantes, + back_populates="eventos" + ) + + def serialize(self): + return { + "evento_id": self.evento_id, + "nombre_evento": self.nombre_evento, + "localizacion": self.localizacion, + "tipo_de_evento": self.tipo_de_evento.value, + } diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..c6ea424793 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,22 +1,689 @@ """ 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.utils import generate_sitemap, APIException + +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, get_jwt +from sqlalchemy import select from flask_cors import CORS +from api.utils import generate_sitemap, APIException +from api.models import db, Profesor, TutorLegal, Estudiantes, Aula, Eventos, SuperAdmin, Asignaturas +from flask import Flask, request, jsonify, url_for, Blueprint -api = Blueprint('api', __name__) -# Allow CORS requests to this API +api = Blueprint('api', __name__) CORS(api) +def admin_required(): + user_id = get_jwt_identity() + user = SuperAdmin.query.get(user_id) + if not user or user.rol_id != 1: + return jsonify({"msg": "Solo admin"}), 403 + return None +def profe_required(): + user_id = get_jwt_identity() + user = Profesor.query.get(user_id) + if not user or user.rol_id != 2: + return jsonify({"msg": "Solo profesor"}), 403 + return None +# def tutorLegal_required(): +# user_id = get_jwt_identity() +# user = TutorLegal.query.get(user_id) +# if not user or user.rol_id != 3: +# return jsonify({"msg": "Solo tutor legal"}), 403 +# return None + +# SUPERADMIN REGISTRO, LOGIN Y GET# + + +@api.route('/superadmin/registro', methods=['POST']) +def registro_superadmin(): + data = request.get_json() + email = data.get('email') + password = data.get('password') + nombre_colegio = data.get('nombre_colegio') + + if not email or not password or not nombre_colegio: + return jsonify({'msg': 'Por favor completar todos los campos'}), 400 + + existing_user = db.session.execute(select(SuperAdmin).where( + SuperAdmin.email == email)).scalar_one_or_none() + + if existing_user: + return jsonify({'msg': 'Ya existe un administrador con este correo'}), 409 + + new_user = SuperAdmin( + email=email, + password=password, + nombre_colegio=nombre_colegio + ) + new_user.set_password(password) + db.session.add(new_user) + db.session.commit() + return jsonify({'msg': 'Administrador creado correctamente'}), 200 + + +@api.route('/superadmin/login', methods=['POST']) +def login_superadmin(): + data = request.get_json() + email = data.get('email') + password = data.get('password') + + if not email or not password: + return jsonify({'msg': 'Correo y contraseña requeridos'}), 400 + + existing_user = db.session.execute(select(SuperAdmin).where( + SuperAdmin.email == email)).scalar_one_or_none() + if existing_user is None: + return jsonify({'msg': 'Correo o contraseña incorrectos'}), 401 + + if existing_user.check_password(password): + access_token = create_access_token(identity=str(existing_user.id), + additional_claims={ + "rol_id": existing_user.rol_id, + "email": existing_user.email, + "nombre_colegio": existing_user.nombre_colegio + } + ) + return jsonify({'access_token': access_token}), 200 + return jsonify({'msg': 'Correo o contraseña incorrectos'}), 401 + +@api.route('perfil/superadmin', methods=['GET']) +@jwt_required() +def perfil_superadmin(): + user = get_jwt_identity() + existing_user = db.session.get(SuperAdmin, int(user["id"])) + if not existing_user: + return jsonify({"msg": "Usuario no encontrado"}), 400 + return jsonify(existing_user.serialize()), 200 + +# CERRAR SESION# + + +@api.route('/logout', methods=['POST']) +@jwt_required() +def logout(): + return jsonify({"msg": "Logout correcto"}), 200 + +# EVENTOS# + + +@api.route('/events', methods=['GET']) +@jwt_required() +def get_events(): + + eventos = Eventos.query.all() + return jsonify([e.serialize() for e in eventos]), 200 + + +@api.route('/eventos/', methods=['PUT']) +@jwt_required() +def update_event(id): + user = get_jwt_identity() + if user["rol_id"] != 2: + return jsonify({"msg": "Solo profesores"}), 403 + evento = Eventos.query.get(id) + if not evento: + return jsonify({"msg": "Evento no encontrado"}), 404 + + data = request.json + evento.nombre_evento = data.get("nombre_evento", evento.nombre_evento) + evento.localizacion = data.get("localizacion", evento.localizacion) + + db.session.commit() + return jsonify(evento.serialize()), 200 + + +@api.route('/eventos/', methods=['DELETE']) +@jwt_required() +def delete_event(id): + user = get_jwt_identity() + if user["rol_id"] not in [1, 2]: + return jsonify({"msg": "Solo profesores"}), 403 + evento = Eventos.query.get(id) + if not evento: + return jsonify({"msg": "Evento no encontrado"}), 404 + db.session.delete(evento) + db.session.commit() + return jsonify({"msg": "Evento eliminado"}), 200 + +# mensajeria# +# @api.route('/messages', methods=['POST']) +# @jwt_required() +# def send_message(): +#      user = get_jwt_identity() +#      if user["rol_id"] not in [2, 3]: +#          return jsonify({"msg": "Solo profesor o tutor"}), 403 +#      return jsonify({"msg": "Mensaje enviado"}), 201 + +# @api.route('/messages', methods=['GET']) +# @jwt_required() +# def get_messages(): +#      user = get_jwt_identity() +#      if user["rol_id"] not in [2, 3]: +#          return jsonify({"msg": "No autorizado"}), 403 +#      return jsonify({"msg": "Lista de mensajes"}), 200 + +# def admin_required(): +#      if get_jwt_identity()["rol_id"] != 1: +#          return jsonify({"msg": "Solo admin"}), 403 + +########REGISTRO PROFESORES########### + + +@api.route('profesor/registro', methods=['POST']) +def registro_profesor(): + data = request.get_json() + name = data.get('name') + email = data.get('email') + rol_id = data.get('rol_id') + telephone = data.get('telephone') + password = data.get('password') + + if not email or not password or not rol_id or not name or not telephone: + return jsonify({'msg': 'Por favor completar todos los campos para completar el registro'}), 400 + + existing_user = db.session.execute(select(Profesor).where( + Profesor.email == email)).scalar_one_or_none() + + if existing_user: + return jsonify({'msg': 'Un perfil de profesor con este correo electrócnico ya existe'}), 409 + + new_user = Profesor(email=email, rol_id=rol_id, + password=password, name=name, telephone=telephone) + new_user.set_password(password) + db.session.add(new_user) + db.session.commit() + return jsonify({'msg': 'El perfil de profesor ha sido creado satisfactoriamente'}), 200 + +#####LOGIN DEL PROFESOR########## + + +@api.route('/profesor/login',methods=['POST']) +def login_profesor(): + profe_id= get_jwt_identity() + claims= get_jwt() + if claims["rol_id"] != 2: + return jsonify({"msg": "Solo profesores"}), 403 + + data= request.get_json() + email= data.get('email') + password= data.get('password') + + if not email or not password: + return jsonify({'msg': 'El correo electrónico y contraseña son requeridos'}), 400 + + existing_user= db.session.get(Profesor, int(profe_id)) + + + + if existing_user.check_password(password): + access_token = create_access_token( + identity=str(existing_user.id), + additional_claims={ + "rol_id": existing_user.rol_id, + "email": existing_user.email, + "aula_id": Aula.aula_id, + "clase": Aula.clase + } + ) + + return jsonify({ + 'msg': 'Inicio de sesión exitoso', + 'token': access_token, + 'existing_user': existing_user.serialize() + }), 200 + + return jsonify({'msg': 'Correo o contraseña incorrectos'}), 401 + +########OBTENER EL LISTADO DE PROFESORES Y SU INFO + +@api.route('/profesores', methods=['GET']) +@jwt_required() +def perfil_profesor(): + existing_user_id = get_jwt_identity() + existing_user = db.session.get(Profesor, int(existing_user_id)) + if not existing_user: + return jsonify({"msg": "Usuario no encontrado"}), 400 + return jsonify(existing_user.serialize()), 200 + +#########MODIFICAR PROFESOR SE PUDEN MODIFICAR ALL MENOS EL ID Y EL ROL_ID PORQUE SI NO SE PUEDEN HACER COSAS RARAS COMO CAMBIAR DE PROFESOR A SUPERADMIN O COSAS ASI############ +@api.route('/profesor/', methods=['PUT']) +@jwt_required() +def update_teacher(id): + admin_check = admin_required() + if admin_check: + return admin_check + teacher = Profesor.query.get(id) + if not teacher: + return jsonify({"msg": "Profesor no encontrado"}), 404 + data = request.json + for key, value in data.items(): + setattr(teacher, key, value) + db.session.commit() + return jsonify(teacher.serialize()), 200 + +########ELIMINAR PROFESOR############ +@api.route('/profesor/', methods=['DELETE']) +@jwt_required() +def delete_teacher(id): + admin_check = admin_required() + if admin_check: + return admin_check + teacher = Profesor.query.get(id) + if not teacher: + return jsonify({"msg": "Profesor no encontrado"}), 404 + db.session.delete(teacher) + db.session.commit() + return jsonify({"msg": "Profesor eliminado"}), 200 + +# ESTUDIANTES# +# ruta para crear estudiante + + +@api.route('/estudiantes', methods=['POST']) +@jwt_required() +def create_student(): + admin_check = admin_required() + if admin_check: + return admin_check + data = request.json + student = Estudiantes( + name=data.get("name"), + profesor_id=data.get("profesor_id"), + aula_id=data.get("aula_id") + ) + db.session.add(student) + db.session.commit() + return jsonify(student.serialize()), 201 + +# ruta para eliminar estudiante + + +@api.route('/estudiantes/', methods=['DELETE']) +@jwt_required() +def delete_student(id): + admin_check = admin_required() + if admin_check: + return admin_check + student = Estudiantes.query.get(id) + if not student: + return jsonify({"msg": "Estudiante no encontrado"}), 404 + db.session.delete(student) + db.session.commit() + return jsonify({"msg": "Estudiante eliminado"}), 200 + + + +######## PARA CREAR AULA SE DEBE ASIGNAR UN CURSO, UNA CLASE Y EL NOMBRE_COLEGIO AL QUE PERTENECE,ADEMAS SE DEBE ASIGNAR UN CURSO Y UNA CLASE PARA IDENTIFICAR EL AULA, POR EJEMPLO CURSO: 1 Y CLASE: A SERIA EL AULA 1A DEL COLEGIO DEL SUPERADMIN QUE HACE LA PETICION############ + +@api.route('/crear/aula', methods=['POST']) +@jwt_required() + +def crear_aula(): + Sadmin_colegio= get_jwt_identity() + claims= get_jwt() + if claims["nombre_colegio"] != "SuperAdmin.nombre_colegio": + return jsonify({"msg": "Solo administradores del colegio"}), 403 + exisisting_user = db.session.get(SuperAdmin, str(Sadmin_colegio)) + if not exisisting_user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + + data = request.json + classroom = Aula( + curso=data.get("curso"), + clase=data.get("clase"), + colegio_id= data.get("SuperAdmin.nombre_colegio") + ) + if classroom.curso is None or classroom.clase is None: + return jsonify({"msg": "Curso, clase y colegio_id son requeridos para crear un aula"}), 400 + + db.session.add(classroom) + db.session.commit() + return jsonify(classroom.serialize()), 201 + +#########ELIMINAR AULA################# SE DEBE ESPECIFICAREL ID Y EL NOMBRE DE LA CLASE PARA ELIMINAR UN AULA, ADEMAS SE DEBE VERIFICAR QUE EL AULA ESTE VACIA, ES DECIR QUE NO TENGA PROFESOR ASIGNADO NI ESTUDIANTES INSCRITOS, SI EL AULA TIENE PROFESOR O ESTUDIANTES NO SE PERMITIRA LA ELIMINACION Y SE MOSTRARA UN MENSAJE DE ERROR INDICANDO QUE EL AULA DEBE ESTAR VACIA PARA SER ELIMINADA, ASI SE ASEGURA QUE NO SE ELIMINEN AULAS QUE ESTEN EN USO Y SE PIERDAN LOS DATOS DE LOS PROFESORES Y ESTUDIANTES ASIGNADOS A ESE AULA############ + +@api.route('/eliminar/aula//', methods=['DELETE']) +@jwt_required() +def eliminar_aula(id, clase): + Sadmin_colegio= get_jwt_identity() + claims= get_jwt() + if claims["nombre_colegio"] != "SuperAdmin.nombre_colegio": + return jsonify({"msg": "Solo administradores del colegio"}), 403 + exisisting_user = db.session.get(SuperAdmin, str(Sadmin_colegio)) + if not exisisting_user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + + admin_check = admin_required() + if admin_check: + return admin_check + + aula = Aula.query.filter_by(aula_id=id, clase=clase).first() + + if not aula: + return jsonify({ + "msg": "No se encontró un aula con ese ID y nombre de clase coincidentes." + }), 404 + + + if aula.profesor_id is not None or len(aula.estudiantes) > 0: + return jsonify({ + "msg": "No se puede eliminar: El aula debe estar totalmente vacía, asigne a los profesor/alumnos antes a otras aulas.", + "detalles": { + "profesor_asignado": aula.profesor_id is not None, + "alumnos_inscritos": len(aula.estudiantes) + } + }), 400 + + db.session.delete(aula) + db.session.commit() + return jsonify({"msg": f"Aula '{clase}' (ID: {id}) eliminada con éxito"}), 200 + + +#####OBTENER AULAS################# NO TIENE JWT PORQUE LO PODRAN VER LOS 3 ROLES. + +@api.route('/aulas', methods=['GET']) +def obtener_aulas(): + + aulas = Aula.query.all() + return jsonify([aula.serialize() for aula in aulas]), 200 + + +#########MODIFICAR AULA AQUI SE PUEDE CAMBIAR LOS DATOS DEL AULA ############ + +@api.route('/aula/', methods=['PUT']) +@jwt_required() + +def actualizar_aula(id): + Sadmin_colegio= get_jwt_identity() + claims= get_jwt() + if claims["nombre_colegio"] != "SuperAdmin.nombre_colegio": + return jsonify({"msg": "Solo administradores del colegio"}), 403 + exisisting_user = db.session.get(SuperAdmin, str(Sadmin_colegio)) + if not exisisting_user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + + admin_check = admin_required() + if admin_check: + return admin_check + + aula = Aula.query.get(id) + if not aula: + return jsonify({"msg": "Aula no encontrada"}), 404 + + data = request.get_json() + aula.curso = data.get("curso", aula.curso) + aula.clase = data.get("clase", aula.clase) + + + db.session.commit() + return jsonify({"msg": "Aula actualizada", "aula": aula.serialize()}), 200 + + +#####CREAR ASIGNATURAS TRONCALES#########SOLO SUPERADMIN LAS PUEDE MODIFICAR,CREAR O ELIMINAR. + +@api.route('/asignaturas', methods=['POST']) +@jwt_required() +def create_asignatura(): + Sadmin_colegio= get_jwt_identity() + claims= get_jwt() + if claims["nombre_colegio"] != "SuperAdmin.nombre_colegio": + return jsonify({"msg": "Solo administradores del colegio"}), 403 + exisisting_user = db.session.get(SuperAdmin, str(Sadmin_colegio)) + if not exisisting_user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + + data = request.get_json() + nueva = Asignaturas(nombre_asignatura=data.get('nombre_asignatura')) + db.session.add(nueva) + db.session.commit() + return jsonify({"msg": "Asignatura troncal creada"}), 201 + +####### MODIFICAR NOMBRE_ASIGNATURA ######## +@api.route('/asignaturas/', methods=['PUT']) +@jwt_required() +def update_asignatura(id): + Sadmin_colegio= get_jwt_identity() + claims= get_jwt() + if claims["nombre_colegio"] != "SuperAdmin.nombre_colegio": + return jsonify({"msg": "Solo administradores del colegio"}), 403 + exisisting_user = db.session.get(SuperAdmin, str(Sadmin_colegio)) + if not exisisting_user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + + + asignatura = db.session.get(Asignaturas, id) + + if not asignatura: + return jsonify({"msg": "La asignatura no existe"}), 404 + + + data = request.get_json() + nuevo_nombre = data.get('nombre_asignatura') + + if not nuevo_nombre: + return jsonify({"msg": "El nuevo nombre de la asignatura es requerido"}), 400 + + + asignatura.nombre_asignatura = nuevo_nombre + db.session.commit() + + return jsonify({ + "msg": "Asignatura actualizada correctamente", + "asignatura": asignatura.serialize() + }), 200 + +######## BORRRAR ASIGNATURA####SOLO X ADMIN INDICANDO EL ID DE LA ASIGNATURA + +@api.route('/asignaturas/', methods=['DELETE']) +@jwt_required() +def delete_asignatura(id): + Sadmin_colegio= get_jwt_identity() + claims= get_jwt() + if claims["nombre_colegio"] != "SuperAdmin.nombre_colegio": + return jsonify({"msg": "Solo administradores del colegio"}), 403 + exisisting_user = db.session.get(SuperAdmin, str(Sadmin_colegio)) + if not exisisting_user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + + asignatura = db.session.get(Asignaturas, id) + if not asignatura: + return jsonify({"msg": "No existe"}), 404 + + db.session.delete(asignatura) + db.session.commit() + return jsonify({"msg": "Asignatura eliminada globalmente"}), 200 + + +########## OBTENER TODAS LAS ASIGNATURAS NO TIENE JWT LAS PUEDEN VER TODOS LOS ROLES###### + +@api.route('/asignaturas', methods=['GET']) + +def get_asignaturas(): + + result = db.session.execute(select(Asignaturas)).scalars().all() + asignaturas_list = [asignatura.serialize() for asignatura in result] + + return jsonify(asignaturas_list), 200 + + + +####CREAR TUTOR LEGAL################## + +@api.route('/tutoresLegales/', methods=['POST']) +@jwt_required() +def create_tutor(): + admin_check = admin_required() + if admin_check: + return admin_check + data = request.json + tutor = TutorLegal( + name=data.get("name"), + email=data.get("email"), + password=data.get("password"), + telephone=data.get("telephone"), + rol_id=data.get("rol_id") + ) + db.session.add(tutor) + db.session.commit() + return jsonify(tutor.serialize()), 201 + +##### MODIFICAR INFORMACION DE LOS TURORES LEGALES######### + + +@api.route('/tutoresLegales/', methods=['PUT']) +@jwt_required() +def update_tutor(id): + admin_check = admin_required() + if admin_check: + return admin_check + tutor = TutorLegal.query.get(id) + if not tutor: + return jsonify({"msg": "Tutor no encontrado"}), 404 + data = request.json + for key, value in data.items(): + setattr(tutor, key, value) + db.session.commit() + return jsonify(tutor.serialize()), 200 + + + + +######## LOGIN DE TUTORES LEGALES ########### + +@api.route('tutorlegal/login', methods=['POST']) +def login_tutor_legal(): + data = request.get_json() + email = data.get('email') + password = data.get('password') + if not email or not password: + return jsonify({'msg': 'El correo electrónico y contraseña son requeridos'}), 400 + existing_user = db.session.execute(select(TutorLegal).where( + TutorLegal.email == email)).scalar_one_or_none() + if existing_user is None: + return jsonify({'msg': 'El correo eletrócnico o contraseña son incorrectos'}), 401 + if existing_user.check_password(password): + access_token = create_access_token(identity=str(existing_user.id)) + return jsonify({'msg': 'Inicio de sesión exitoso', 'token': access_token, 'existing_user': existing_user.serialize()}), 200 + else: + return jsonify({'msg': 'El correo eletrócnico o contraseña son incorrectos'}), 401 + +########## OBTENER PERFIL DE TUTOR LEGAL########### + +@api.route('/perfil/tutorlegal', methods=['GET']) +@jwt_required() +def perfil_tutorlegal(): + existing_user_id = get_jwt_identity() + existing_user = db.session.get(TutorLegal, int(existing_user_id)) + if not existing_user: + return jsonify({"msg": "Usuario no encontrado"}), 400 + return jsonify(existing_user.serialize()), 200 + +#######REGISTRO TUTOR LEGAL SOLO SUPERADMIN LO HACE######## + +@api.route('tutorlegal/registro', methods=['POST']) +def registro_tutorlegal(): + data = request.get_json() + name = data.get('name') + email = data.get('email') + rol_id = data.get('rol_id') + telephone = data.get('telephone') + password = data.get('password') + if not email or not password or not rol_id or not name or not telephone: + return jsonify({'msg': 'Por favor completar todos los campos para completar el registro'}), 400 + existing_user = db.session.execute(select(TutorLegal).where( + TutorLegal.email == email)).scalar_one_or_none() + if existing_user: + return jsonify({'msg': 'Un perfil de administrador con este correo electrócnico ya existe'}), 409 + new_user = TutorLegal(email=email, rol_id=rol_id, + password=password, name=name, telephone=telephone) + new_user.set_password(password) + db.session.add(new_user) + db.session.commit() + return jsonify({'msg': 'El perfil de administrador ha sido creado satisfactoriamente'}), 200 + + +# ESTOS ENDPOINTS ESTAN COMENTADOS PORQUE AUN NO LOS HE COMPROBADO EN POSTMAN +# @api.route('calificaciones/crear', methods=['POST']) +# @jwt_required() +# def crear_calificaciones(): +# existing_user_id= get_jwt_identity() +# existing_user= db.session.get(Profesor,int(existing_user_id)) + +# if not existing_user: +# return jsonify({"msg":"Usuario no autorizado"}),401 + +# data= request.get_json() + +# if not data: +# return jsonify({"msg":"Datos Inválidos"}),400 + +# estudiante_id= data.get("estudiante_id") +# estudiante= db.session.get(Estudiantes,estudiante_id) + +# if not estudiante: +# return jsonify({"msg": "Estudiante no encontrado"}), 404 + +# if estudiante.profesor_id != existing_user_id: +# return jsonify ({"msg": "Este estudiante no es tuyo, no puedes modificarlo"}), 404 + + +# nueva_calificacion= Calificaciones( +# calificacion= data.get("calificacion"), +# estudiante_id=data.get("estudiante_id"), +# asignatura_id=data.get("asignatura_id") +# ) + +# db.session.add(nueva_calificacion) +# db.session.commit() + +# return jsonify(nueva_calificacion.serialize()),201 + + +# @api.route('calificaciones/editar/', methods=['PUT']) +# @jwt_required() +# def editar_calificaciones(calificacion_id): +# existing_user_id= get_jwt_identity() +# existing_user = db.session.get(Calificaciones,int(existing_user_id)) +# if not existing_user: +# return jsonify({'msg':'Usuario no autorizado'}),400 + +# calificacion= db.session.get(Calificaciones, calificacion_id) + +# if not calificacion: +# return jsonify({'msg':'Calificaion no encontrada'}),404 + +# data= request.get_json() + +# if "calificacion" in data: +# calificacion.calificacion_id= data["calificacion"] + +# if "estudiante_id" in data: +# calificacion.estudiante_id=data["estudiante_id"] + +# if "asignatura_id" in data: +# calificacion.asignatura_id=data["asignatura_id"] + +# db.session.commit() + +# return jsonify(calificacion.serialize()),200 + + +# @api.route('calificaciones/eliminar/', methods=['DELETE']) +# @jwt_required() +# def eliminar_calificaciones(calificacion_id): +# existing_user_id= get_jwt_identity() +# existing_user = db.session.get(Calificaciones,int(existing_user_id)) + +# if not existing_user: +# return jsonify({'msg':'Usuario no autorizado'}),400 + +# calificacion= db.session.get(Calificaciones, calificacion_id) -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): +# if not calificacion: +# return jsonify({'msg':'Calificaion no encontrada'}),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" - } +# db.session.delete(calificacion) +# db.session.commit() - return jsonify(response_body), 200 +# return jsonify({"msg":"la calificación ha sido eliminada exitosamente"}),200 diff --git a/src/app.py b/src/app.py index 1b3340c0fa..a0befbae2a 100644 --- a/src/app.py +++ b/src/app.py @@ -9,7 +9,11 @@ from api.models import db from api.routes import api from api.admin import setup_admin -from api.commands import setup_commands +# from api.commands import setup_commands + +from datetime import timedelta + +from flask_jwt_extended import JWTManager # from models import Person @@ -17,8 +21,14 @@ static_file_dir = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../dist/') app = Flask(__name__) + app.url_map.strict_slashes = False +# Setup the Flask-JWT-Extended extension +app.config['JWT-SECRET-KEY'] = os.getenv('JWT-SECRET-KEY') +app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=24) +jwt = JWTManager(app) + # database condiguration db_url = os.getenv("DATABASE_URL") if db_url is not None: @@ -35,7 +45,7 @@ setup_admin(app) # add the admin -setup_commands(app) +# setup_commands(app) # Add all endpoints form the API with a "api" prefix app.register_blueprint(api, url_prefix='/api') @@ -69,4 +79,4 @@ def serve_any_other_file(path): # this only runs if `$ python src/main.py` is executed if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) - app.run(host='0.0.0.0', port=PORT, debug=True) + app.run(host='0.0.0.0', port=PORT, debug=True) \ No newline at end of file diff --git a/src/front/pages/LandingPagePT.jsx b/src/front/pages/LandingPagePT.jsx new file mode 100644 index 0000000000..9f4d9a83ee --- /dev/null +++ b/src/front/pages/LandingPagePT.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +export const LandingPagePT = () => { + const primaryColor = "#6200e8"; + const navigate = useNavigate(); + + return ( +
+ +
+
+
+ + + +

+ VIP CC +

+ +

+ Portal Educativo +

+ +
+ + + + + + +
+ +
+ © 2026 VIP CC • Transformando la educación +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..fd257a1ff9 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -9,6 +9,8 @@ import { Layout } from "./pages/Layout"; import { Home } from "./pages/Home"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; +import { LandingPagePT } from "./pages/LandingPagePT"; + export const router = createBrowserRouter( createRoutesFromElements( @@ -25,6 +27,7 @@ export const router = createBrowserRouter( } /> } /> {/* Dynamic route for single items */} } /> + } /> ) ); \ No newline at end of file